Compare commits

..

70 Commits

Author SHA1 Message Date
Franz Heinzmann (Frando)
7147d32601 fix 2022-07-15 16:24:04 +02:00
Franz Heinzmann (Frando)
343bb34589 Fix rename 2022-07-15 15:49:42 +02:00
Franz Heinzmann (Frando)
361b7f5b69 Add API versioning to the JSON-RPC API 2022-07-15 14:51:28 +02:00
Simon Laux
15019ce02b use camelCase in all js object properties 2022-07-15 13:48:32 +02:00
Simon Laux
7bb5dc4c3c add chat_get_media to jsonrpc
also add viewtype wrapper enum and use it in `MessageObject`,
additionally to using it in `chat_get_media`
2022-07-09 17:30:55 +02:00
Simon Laux
543edac105 add webxdc methods to jsonrpc:
- `webxdc_send_status_update`
- `webxdc_get_status_updates`
- `message_get_webxdc_info`
2022-07-09 17:02:11 +02:00
Simon Laux
55db8d1fe0 jsonrpc: add dm_chat_contact to ChatListItemFetchResult 2022-07-05 15:55:40 +02:00
Simon Laux
1d92f06834 jsonrpc: add get_fresh_msgs and get_fresh_msg_cnt 2022-07-05 15:55:40 +02:00
Simon Laux
2ff05c9dff add add_device_message to jsonrpc 2022-07-05 15:55:40 +02:00
Simon Laux
d6b6d96e21 add set_config_from_qr to jsonrpc 2022-07-05 15:55:40 +02:00
Simon Laux
8776767a44 make dcn_json_rpc_request return undefined instead of not returning
this might have been the cause for the second segfault
2022-07-05 15:55:40 +02:00
Simon Laux
9d83057a71 remove print from test 2022-07-05 15:55:40 +02:00
Simon Laux
8ce11ac62f found another segfault:
this time in batch_set_config
2022-07-05 15:55:40 +02:00
Simon Laux
918ec85767 restore same configure behaviour as desktop:
make configure restart io with the old configuration if it had one on error
2022-07-05 15:55:40 +02:00
Simon Laux
f3a9ab6d23 fix the typos
thanks to ralphtheninja for finding them
2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
e4def6a44d Increase online test timeouts for CI 2022-07-05 15:55:40 +02:00
Simon Laux
4b5c194ef3 make sure to use dc_str_unref instead of free
on cstrings returned/owned by rust
2022-07-05 15:55:40 +02:00
Simon Laux
97e2d85b28 Update deltachat-ffi/deltachat.h
Co-authored-by: bjoern <r10s@b44t.com>
2022-07-05 15:55:40 +02:00
Simon Laux
81bc7bd7bf Update deltachat-ffi/deltachat.h
Co-authored-by: bjoern <r10s@b44t.com>
2022-07-05 15:55:40 +02:00
Simon Laux
f53c456e50 remove unneeded context
thanks to link2xt for pointing that out
2022-07-05 15:55:40 +02:00
Simon Laux
64fa5675a9 reintroduce segfault test script 2022-07-05 15:55:40 +02:00
Simon Laux
1f0bdfa704 apply link2xt's suggestions:
- unref jsonrpc_instance in same thread it was created in
- increase `max_queue_size` from 1 to 1000
2022-07-05 15:55:40 +02:00
Simon Laux
5ac347b7ae make it more idiomatic:
rename `ContactObject::from_dc_contact -> `ContactObject::try_from_dc_contact`
2022-07-05 15:55:40 +02:00
Simon Laux
40fa2d4120 adress dig's comments
- description in cargo.toml
- impl From<EventType> for EventTypeName
- rename `CommandApi::new_from_arc` -> `CommandApi::from_arc`
- pre-allocate if we know the entry count already
- remove unused enumerate
- remove unused serde attribute comment
- rename `FullChat::from_dc_chat_id` -> `FullChat::try_from_dc_chat_id`
2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
aaf27e4434 fix docs 2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
2df10857ca Improve documentation 2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
7eae3a1072 Naming consistency: Use DeltaChat not Deltachat 2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
7fc162543a Improve JSON-RPC CI, no need to build things multiple times 2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
e7da0672ae Fix method name casings and cleanups 2022-07-05 15:55:38 +02:00
Simon Laux
69d9d48ae4 changelog entry about the jsonrpc 2022-07-05 15:54:31 +02:00
Franz Heinzmann (Frando)
802677222b remove debug logs 2022-07-05 15:54:00 +02:00
Simon Laux
d7b9febc33 update todo document
remove specific api stuff for now,
we now have the an incremental aproach on moving
not the all at-once effort I though it would be
2022-07-05 15:54:00 +02:00
Franz Heinzmann (Frando)
1d347f7369 Bump yerpc to 0.3.1 with fix for axum server 2022-07-05 15:53:58 +02:00
Franz Heinzmann (Frando)
e3fa42fe88 don't wait for IO on webserver start 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
2f00b098ac improve test setup and code style 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
bdd4aa0f10 use multi-threaded runtime in JSON-RPC webserver 2022-07-05 15:50:03 +02:00
Simon Laux
6fee4fd878 expose anyhow errors
feature name was wrong
2022-07-05 15:50:03 +02:00
Simon Laux
60d3a3cacf remove emtpy file
allow unused code for new_from_arc
2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
4bb1980f8d try to fix ci 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
35b70b1d1b fix ci 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
fd53b80c17 use stable toolchain not 1.56.0 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
978e4aec82 Fix CFFI for JSON-RPC changes 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
329f498651 improve docs, fix example 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
035e208e4f Improve docs. 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
3404996fdd Improvements to typescript package 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
97e0e0137a Changes for tokio compat, upgrade to yerpc 0.3
This also changes the webserver binary to use axum in place of tide.
2022-07-05 15:49:59 +02:00
Simon Laux
659e48bd3f improve naming 2022-07-05 15:36:45 +02:00
Simon Laux
271d54e420 commit types.ts
that dc-node has everything it needs to provide @deltachat/jsonrpc-client
without an extra ts compile step
2022-07-05 15:36:45 +02:00
Simon Laux
9984ee5eb2 add @deltachat/jsonrpc-client
to make sure its dependencies are installed, too
whwn installing dc-node
2022-07-05 15:36:45 +02:00
Simon Laux
136bec0273 disable jsonrpc by default 2022-07-05 15:36:45 +02:00
Simon Laux
c5ff7427be put jsonrpc stuff in own module 2022-07-05 15:36:45 +02:00
Simon Laux
2319dfc3eb remove selectAccount from highlevel client 2022-07-05 15:36:45 +02:00
Simon Laux
d8d26b9cae activate other tests again 2022-07-05 15:36:45 +02:00
Simon Laux
d93622bc84 fix closing segfault
thanks again to link2xt for figguring this out
2022-07-05 15:36:45 +02:00
Simon Laux
2fde4962a1 break loop on empty response 2022-07-05 15:36:45 +02:00
Simon Laux
29a5d73f94 call a jsonrpc function in segfault example 2022-07-05 15:36:45 +02:00
Simon Laux
b51814aaaa add jsonrpc feature flag 2022-07-05 15:36:45 +02:00
Simon Laux
63e7179191 add jsonrpc crate to set_core_version 2022-07-05 15:36:45 +02:00
Simon Laux
9915803252 add some files to npm ignore
that don't need to be in the npm package
2022-07-05 15:36:45 +02:00
Simon Laux
e12aeb7bd8 add json api to cffi and expose it in dc node 2022-07-05 15:36:45 +02:00
Simon Laux
346fab7f26 fix compile after rebase 2022-07-05 15:36:45 +02:00
Simon Laux
6f6e6f24c9 change now returns event names as id
directly, no conversion method or number ids anymore

also longer timeout for requesting test accounts from mailadm
2022-07-05 15:36:45 +02:00
Simon Laux
079cd67da8 update .gitignore 2022-07-05 15:36:44 +02:00
Simon Laux
bfd97fdb05 fix formatting
make test  pass
fix clippy
2022-07-05 15:36:44 +02:00
Simon Laux
688326f21c refactor function name 2022-07-05 15:36:44 +02:00
Simon Laux
cc20d25b8d fix get_provider_info docs 2022-07-05 15:36:44 +02:00
Simon Laux
99d50615c3 use node 16 in ci
use `npm i` instead of `npm ci`
try fix ci script
and fix a doc comment
2022-07-05 15:36:44 +02:00
Simon Laux
a006825376 fix clippy 2022-07-05 15:36:44 +02:00
Simon Laux
c90fd1c9ce get target dir from cargo 2022-07-05 15:36:44 +02:00
Simon Laux
bf5d09e74a integrate json-rpc repo
https://github.com/deltachat/deltachat-jsonrpc
2022-07-05 15:36:44 +02:00
111 changed files with 1997 additions and 6036 deletions

View File

@@ -16,7 +16,7 @@ mergeable:
required: ['CHANGELOG.md']
- do: dependent
changed:
file: 'deltachat-ffi/src/**'
file: 'deltachat-ffi/**'
required: ['CHANGELOG.md']
fail:
- do: checks

View File

@@ -45,7 +45,7 @@ jobs:
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --tests --examples --benches --features repl -- -D warnings
args: --workspace --tests --examples --benches
docs:
name: Rust doc comments
@@ -116,11 +116,6 @@ jobs:
command: test
args: --all
- name: test cargo vendor
uses: actions-rs/cargo@v1
with:
command: vendor
- name: install python
if: ${{ matrix.python }}
uses: actions/setup-python@v4

View File

@@ -1,83 +0,0 @@
name: 'jsonrpc js client build'
on:
pull_request:
push:
tags:
- '*'
- '!py-*'
jobs:
pack-module:
name: 'Package @deltachat/jsonrpc-client and upload to download.delta.chat'
runs-on: ubuntu-18.04
steps:
- name: install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: get tag
id: tag
uses: dawidd6/action-get-tag@v1
continue-on-error: true
- name: Get Pullrequest ID
id: prepare
run: |
tag=${{ steps.tag.outputs.tag }}
if [ -z "$tag" ]; then
node -e "console.log('DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
else
echo "DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
echo "No preview will be uploaded this time, but the $tag release"
fi
- name: System info
run: |
npm --version
node --version
echo $DELTACHAT_JSONRPC_TAR_GZ
- name: install dependencies without running scripts
run: |
cd deltachat-jsonrpc/typescript
npm install --ignore-scripts
- name: package
shell: bash
run: |
cd deltachat-jsonrpc/typescript
npm run build:tsc
npm pack .
ls -lah
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v1
with:
name: deltachat-jsonrpc-client.tgz
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
# Upload to download.delta.chat/node/preview/
- name: Upload deltachat-jsonrpc-client preview to download.delta.chat/node/preview/
if: ${{ ! steps.tag.outputs.tag }}
id: upload-preview
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
continue-on-error: true
- name: "Post links to details"
if: steps.upload-preview.outcome == 'success'
run: node ./node/scripts/postLinksToDetails.js
env:
URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MSG_CONTEXT: Download the deltachat-jsonrpc-client.tgz
# Upload to download.delta.chat/node/
- name: Upload deltachat-jsonrpc-client build to download.delta.chat/node/
if: ${{ steps.tag.outputs.tag }}
id: upload
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"

View File

@@ -3,181 +3,27 @@
## Unreleased
### API-Changes
### Changes
### Fixes
## 1.96.0
### Changes
- jsonrpc js client:
- Change package name from `deltachat-jsonrpc-client` to `@deltachat/jsonrpc-client`
- remove relative file dependency to it from `deltachat-node` (because it did not work anyway and broke the nix build of desktop)
- ci: add github ci action to upload it to our download server automaticaly on realease
## 1.95.0
### API-Changes
- jsonrpc: add `mailingListAddress` property to `FullChat` #3607
- jsonrpc: add `MessageNotificationInfo` & `messageGetNotificationInfo()` #3614
- jsonrpc: add `chat_get_neighboring_media` function #3610
### Changes
- added `dclogin:` scheme to allow configuration from a qr code
(data inside qrcode, contrary to `dcaccount:` which points to an API to create an account) #3541
- truncate incoming messages by lines instead of just length #3480
- emit separate `DC_EVENT_MSGS_CHANGED` for each expired message,
and `DC_EVENT_WEBXDC_INSTANCE_DELETED` when a message contains a webxdc #3605
- enable `bcc_self` by default #3612
## 1.94.0
### API-Changes
- breaking change: replace `dc_accounts_event_emitter_t` with `dc_event_emitter_t` #3422
Type `dc_accounts_event_emitter_t` is removed.
`dc_accounts_get_event_emitter()` returns `dc_event_emitter_t` now, so
`dc_get_next_event()` should be used instead of `dc_accounts_get_next_event`
and `dc_event_emitter_unref()` should be used instead of
`dc_accounts_event_emitter_unref`.
- add `dc_contact_was_seen_recently()` #3560
- Fix `get_connectivity_html` and `get_encrinfo` futures not being Send. See rust-lang/rust#101650 for more information
- jsonrpc: add functions: #3586, #3587, #3590
- `deleteChat()`
- `getChatEncryptionInfo()`
- `getChatSecurejoinQrCodeSvg()`
- `leaveGroup()`
- `removeContactFromChat()`
- `addContactToChat()`
- `deleteMessages()`
- `getMessageInfo()`
- `getBasicChatInfo()`
- `marknoticedChat()`
- `getFirstUnreadMessageOfChat()`
- `markseenMsgs()`
- `forwardMessages()`
- `removeDraft()`
- `getDraft()`
- `miscSendMsg()`
- `miscSetDraft()`
- `maybeNetwork()`
- `getConnectivity()`
- `getContactEncryptionInfo()`
- `getConnectivityHtml()`
- jsonrpc: add `is_broadcast` property to `ChatListItemFetchResult` #3584
- jsonrpc: add `was_seen_recently` property to `ChatListItemFetchResult`, `FullChat` and `Contact` #3584
- jsonrpc: add `webxdc_info` property to `Message` #3588
- python: move `get_dc_event_name()` from `deltachat` to `deltachat.events` #3564
- jsonrpc: add `webxdc_info`, `parent_id` and `download_state` property to `Message` #3588, #3590
- jsonrpc: add `BasicChat` object as a leaner alternative to `FullChat` #3590
- jsonrpc: add `last_seen` property to `Contact` #3590
- breaking! jsonrpc: replace `Message.quoted_text` and `Message.quoted_message_id` with `Message.quote` #3590
- add separate stock strings for actions done by contacts to make them easier to translate #3518
- `dc_initiate_key_transfer()` is non-blocking now. #3553
UIs don't need to display a button to cancel sending Autocrypt Setup Message with
`dc_stop_ongoing_process()` anymore.
### Changes
- order contact lists by "last seen";
this affects `dc_get_chat_contacts()`, `dc_get_contacts()` and `dc_get_blocked_contacts()` #3562
- add `internet_access` flag to `dc_msg_get_webxdc_info()` #3516
- `DC_EVENT_WEBXDC_INSTANCE_DELETED` is emitted when a message containing a webxdc gets deleted #3592
### Fixes
- do not emit notifications for blocked chats #3557
- Show attached .eml files correctly #3561
- Auto accept contact requests if `Config::Bot` is set for a client #3567
- Don't prepend the subject to chat messages in mailinglists
- fix `set_core_version.py` script to also update version in `deltachat-jsonrpc/typescript/package.json` #3585
- Reject webxcd-updates from contacts who are not group members #3568
## 1.93.0
### API-Changes
- added a JSON RPC API, accessible through a WebSocket server, the CFFI bindings and the Node.js bindings #3463 #3554 #3542
- JSON RPC methods in CFFI #3463:
- jsonrpc api over websocket server (basically a new api next to the cffi) #3463
- jsonrpc methods in cffi #3463:
- `dc_jsonrpc_instance_t* dc_jsonrpc_init(dc_accounts_t* account_manager);`
- `void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);`
- `void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, char* request);`
- `char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);`
- node: JSON RPC methods #3463:
- node: json rpc methods #3463:
- `AccountManager.prototype.startJsonRpcHandler(callback: ((response: string) => void)): void`
- `AccountManager.prototype.jsonRpcRequest(message: string): void`
### Changes
- use [pathlib](https://docs.python.org/3/library/pathlib.html) in provider update script #3543
- `dc_get_chat_media()` can return media globally #3528
- node: add `getMailinglistAddr()` #3524
- avoid duplicate encoded-words package and test `cargo vendor` in ci #3549
- python: don't raise an error if addr changes #3530
- improve coverage script #3530
### Fixes
- improved error handling for account setup from qrcode #3474
- python: enable certificate checks in cloned accounts #3443
## 1.92.0
### API-Changes
- add `dc_chat_get_mailinglist_addr()` #3520
## 1.91.0
### Added
- python bindings: extra method to get an account running
### Changes
- refactorings #3437
### Fixes
- mark "group image changed" as system message on receiver side #3517
## 1.90.0
### Changes
- handle drafts from mailto links in scanned QR #3492
- do not overflow ratelimiter leaky bucket #3496
- (AEAP) Add device message after you changed your address #3505
- (AEAP) Revert #3491, instead only replace contacts in verified groups #3510
- improve python bindings and tests #3502 #3503
### Fixes
- don't squash text parts of NDN into attachments #3497
- do not treat non-failed DSNs as NDNs #3506
## 1.89.0
### Changes
- (AEAP) When one of your contacts changed their address, they are
only replaced in the chat where you got a message from them
for now #3491
### Fixes
- replace musl libc name resolution errors with a better message #3485
- handle updates for not yet downloaded webxdc instances #3487
## 1.88.0
### Changes
- Implemented "Automatic e-mail address Porting" (AEAP). You can
configure a new address in DC now, and when receivers get messages
they will automatically recognize your moving to a new address. #3385
- added a JSON RPC API, accessible through a WebSocket server, the CFFI bindings and the Node.js bindings #3463
- switch from `async-std` to `tokio` as the async runtime #3449
- upgrade to `pgp@0.8.0` #3467
- add IMAP ID extension support #3468
- configure DeltaChat folder by selecting it, so it is configured even if not LISTed #3371
- build PyPy wheels #6683
- improve default error if NDN does not provide an error #3456
- increase ratelimit from 3 to 6 messages per 60 seconds #3481
### Fixes
- mailing list: remove square-brackets only for first name #3452

466
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.96.0"
version = "1.87.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
license = "MPL-2.0"
@@ -32,11 +32,11 @@ bitflags = "1.3"
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
dirs = { version = "4", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
encoded-words = "0.2"
escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.24.3", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
@@ -46,7 +46,7 @@ native-tls = "0.2"
num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.13.1"
once_cell = "1.12.0"
percent-encoding = "2.0"
pgp = { version = "0.8", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
@@ -54,10 +54,10 @@ quick-xml = "0.23"
r2d2 = "0.8"
r2d2_sqlite = "0.20"
rand = "0.8"
regex = "1.6"
regex = "1.5"
rusqlite = { version = "0.27", features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustyline = { version = "10", optional = true }
rustyline = { version = "9", optional = true }
sanitize-filename = "0.4"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
@@ -83,7 +83,7 @@ async_zip = { git = "https://github.com/dignifiedquire/rs-async-zip", branch = "
[dev-dependencies]
ansi_term = "0.12.0"
criterion = { version = "0.3.6", features = ["async_tokio"] }
criterion = { version = "0.3.4", features = ["async_tokio"] }
futures-lite = "1.12"
log = "0.4"
pretty_env_logger = "0.4"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.96.0"
version = "1.87.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -25,7 +25,7 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
anyhow = "1"
thiserror = "1"
rand = "0.7"
once_cell = "1.13.1"
once_cell = "1.12.0"
[features]
default = ["vendored"]

View File

@@ -22,11 +22,9 @@ typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
// Alias for backwards compatibility, use dc_event_emitter_t instead.
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
/**
* @mainpage Getting started
*
@@ -393,8 +391,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* If no type is prefixed, the videochat is handled completely in a browser.
* - `bot` = Set to "1" if this is a bot.
* Prevents adding the "Device messages" and "Saved messages" chats,
* adds Auto-Submitted header to outgoing messages
* and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
* adds Auto-Submitted header to outgoing messages.
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
@@ -468,10 +465,10 @@ int dc_set_stock_translation(dc_context_t* context, uint32_t stock_i
/**
* Set configuration values from a QR code.
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT, DC_QR_LOGIN or DC_QR_WEBRTC_INSTANCE.
* QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
*
* Internally, the function will call dc_set_config() with the appropriate keys,
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
*
* @memberof dc_context_t
@@ -1266,7 +1263,7 @@ void dc_marknoticed_chat (dc_context_t* context, uint32_t ch
/**
* Returns all message IDs of the given types in a given chat or any chat.
* Returns all message IDs of the given types in a chat.
* Typically used to show a gallery.
* The result must be dc_array_unref()'d
*
@@ -1276,8 +1273,7 @@ void dc_marknoticed_chat (dc_context_t* context, uint32_t ch
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id >0: get messages with media from this chat ID.
* 0: get messages with media from any chat of the currently used account.
* @param chat_id The chat ID to get all messages with media from.
* @param msg_type Specify a message type to query here, one of the @ref DC_MSG constants.
* @param msg_type2 Alternative message type to search for. 0 to skip.
* @param msg_type3 Alternative message type to search for. 0 to skip.
@@ -1288,6 +1284,7 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
/**
* Search next/previous message based on a given message and a list of types.
* The
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
@@ -1388,9 +1385,6 @@ void dc_block_chat (dc_context_t* context, uint32_t ch
*
* Use it to accept "contact request" chats as indicated by dc_chat_is_contact_request().
*
* If the dc_set_config()-option `bot` is set,
* all chats are accepted automatically and calling this function has no effect.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to accept.
@@ -2174,10 +2168,11 @@ char* dc_imex_has_backup (dc_context_t* context, const char*
* ~~~
*
* After that, this function should be called to send the Autocrypt Setup Message.
* The function creates the setup message and adds it to outgoing message queue.
* The message is sent asynchronously.
* The function creates the setup message and waits until it is really sent.
* As this may take a while, it is recommended to start the function in a separate thread;
* to interrupt it, you can use dc_stop_ongoing_process().
*
* The required setup code is returned in the following format:
* After everything succeeded, the required setup code is returned in the following format:
*
* ~~~
* 1234-1234-1234-1234-1234-1234-1234-1234-1234
@@ -2243,8 +2238,8 @@ int dc_continue_key_transfer (dc_context_t* context, uint32_t ms
* The ongoing process will return ASAP then, however, it may
* still take a moment.
*
* Typical ongoing processes are started by dc_configure()
* or dc_imex(). As there is always at most only
* Typical ongoing processes are started by dc_configure(),
* dc_initiate_key_transfer() or dc_imex(). As there is always at most only
* one onging process at the same time, there is no need to define _which_ process to exit.
*
* @memberof dc_context_t
@@ -2270,7 +2265,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_WITHDRAW_VERIFYGROUP 502 // text1=groupname
#define DC_QR_REVIVE_VERIFYCONTACT 510
#define DC_QR_REVIVE_VERIFYGROUP 512 // text1=groupname
#define DC_QR_LOGIN 520 // text1=email_address
/**
* Check a scanned QR code.
@@ -2298,7 +2292,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID:
* scanned fingerprint does not match last seen fingerprint.
*
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::text1=Formatted fingerprint
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
* the scanned QR code contains a fingerprint but no e-mail address;
* suggest the user to establish an encrypted connection first.
*
@@ -2311,8 +2305,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
* if so, call dc_set_config_from_qr().
*
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
* e-mail address scanned, optionally, a draft message could be set in
* dc_lot_t::text1 in which case dc_lot_t::text1_meaning will be DC_TEXT1_DRAFT;
* e-mail address scanned,
* ask the user if they want to start chatting;
* if so, call dc_create_chat_by_contact_id().
*
@@ -2343,10 +2336,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
* ask the user if they want to revive the withdrawn group-invite code;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_LOGIN with dc_lot_t::text1=email_address:
* ask the user if they want to login with the email_address,
* if so, call dc_set_config_from_qr() and then dc_configure().
*
* @memberof dc_context_t
* @param context The context object.
* @param qr The text of the scanned QR code.
@@ -2520,9 +2509,9 @@ int dc_set_location (dc_context_t* context, double latit
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
* 0 for "all up to now".
* @return An array of locations, NULL is never returned.
* The array is sorted descending;
* The array is sorted decending;
* the first entry in the array is the location with the newest timestamp.
* Note that this is only related to the recent position of the user
* Note that this is only realated to the recent postion of the user
* if dc_array_is_independent() returns 0.
* The returned array must be freed using dc_array_unref().
*
@@ -2846,7 +2835,7 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
*
* The library will emit various @ref DC_EVENT events as "new message", "message read" etc.
* To get these events, you have to create an event emitter using this function
* and call dc_get_next_event() on the emitter.
* and call dc_accounts_get_next_event() on the emitter.
*
* This is similar to dc_get_event_emitter(), which, however,
* must not be called for accounts handled by the account manager.
@@ -2854,13 +2843,13 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
* @memberof dc_accounts_t
* @param accounts The account manager as created by dc_accounts_new().
* @return Returns the event emitter, NULL on errors.
* Must be freed using dc_event_emitter_unref() after usage.
* Must be freed using dc_accounts_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per account manager.
* Having more than one event emitter running at the same time on the same account manager
* will result in events randomly delivered to the one or to the other.
*/
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
dc_accounts_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
/**
@@ -3267,19 +3256,6 @@ uint32_t dc_chat_get_id (const dc_chat_t* chat);
int dc_chat_get_type (const dc_chat_t* chat);
/**
* Returns the address where messages are sent to if the chat is a mailing list.
* If you just want to know if a mailing list can be written to,
* use dc_chat_can_send() instead.
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return The mailing list address. Must be released using dc_str_unref() after usage.
* If there is no such address, an empty string is returned, NULL is never returned.
*/
char* dc_chat_get_mailinglist_addr (const dc_chat_t* chat);
/**
* Get name of a chat. For one-to-one chats, this is the name of the contact.
* For group chats, this is the name given e.g. to dc_create_group_chat() or
@@ -3767,11 +3743,6 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
* URL where the source code of the Webxdc and other information can be found;
* defaults to an empty string.
* Implementations may offer an menu or a button to open this URL.
* - internet_access:
* true if the Webxdc should get full internet access, including Webrtc.
* currently, this is only true for encrypted Webxdc's in the self chat
* that have requested internet access in the manifest.
* this is useful for development and maybe for internal integrations at some point.
*
* @memberof dc_msg_t
* @param msg The webxdc instance.
@@ -4653,22 +4624,6 @@ char* dc_contact_get_status (const dc_contact_t* contact);
*/
int64_t dc_contact_get_last_seen (const dc_contact_t* contact);
/**
* Check if the contact was seen recently.
*
* The UI may highlight these contacts,
* eg. draw a little green dot on the avatars of the users recently seen.
* DC_CONTACT_ID_SELF and other special contact IDs are defined as never seen recently (they should not get a dot).
* To get the time a contact was seen, use dc_contact_get_last_seen().
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 1=contact seen recently, 0=contact not seen recently.
*/
int dc_contact_was_seen_recently (const dc_contact_t* contact);
/**
* Check if a contact is blocked.
*
@@ -5276,8 +5231,9 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
* @class dc_event_emitter_t
*
* Opaque object that is used to get events from a single context.
* You can get an event emitter from a context using dc_get_event_emitter()
* or dc_accounts_get_event_emitter().
* You can get an event emitter from a context using dc_get_event_emitter().
* If you are using the dc_accounts_t account manager,
* dc_accounts_event_emitter_t must be used instead.
*/
/**
@@ -5293,8 +5249,6 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
*/
dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
// Alias for backwards compatibility, use dc_get_next_event instead.
#define dc_accounts_get_next_event dc_get_next_event
/**
* Free a context event emitter object.
@@ -5305,8 +5259,39 @@ dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
*/
void dc_event_emitter_unref(dc_event_emitter_t* emitter);
// Alias for backwards compatibility, use dc_event_emtitter_unref instead.
#define dc_accounts_event_emitter_unref dc_event_emitter_unref
/**
* @class dc_accounts_event_emitter_t
*
* Opaque object that is used to get events from the dc_accounts_t account manager.
* You get an event emitter from the account manager using dc_accounts_get_event_emitter().
* If you are not using the dc_accounts_t account manager but just a single dc_context_t object,
* dc_event_emitter_t must be used instead.
*/
/**
* Get the next event from an accounts event emitter object.
*
* @memberof dc_accounts_event_emitter_t
* @param emitter Event emitter object as returned from dc_accounts_get_event_emitter().
* @return An event as an dc_event_t object.
* You can query the event for information using dc_event_get_id(), dc_event_get_data1_int() and so on;
* if you are done with the event, you have to free the event using dc_event_unref().
* If NULL is returned, the contexts belonging to the event emitter are unref'd and no more events will come;
* in this case, free the event emitter using dc_accounts_event_emitter_unref().
*/
dc_event_t* dc_accounts_get_next_event (dc_accounts_event_emitter_t* emitter);
/**
* Free an accounts event emitter object.
*
* @memberof dc_accounts_event_emitter_t
* @param emitter Event emitter object as returned from dc_accounts_get_event_emitter().
* If NULL is given, nothing is done and an error is logged.
*/
void dc_accounts_event_emitter_unref(dc_accounts_event_emitter_t* emitter);
/**
* @class dc_event_t
@@ -5378,7 +5363,7 @@ char* dc_event_get_data2_str(dc_event_t* event);
* To get the context object belonging to the event, use dc_accounts_get_account().
*
* @memberof dc_event_t
* @param event The event object as returned from dc_get_next_event().
* @param event The event object as returned from dc_accounts_get_next_event().
* @return The account ID belonging to the event, 0 for account manager errors.
*/
uint32_t dc_event_get_account_id(dc_event_t* event);
@@ -5728,15 +5713,7 @@ void dc_event_unref(dc_event_t* event);
* @param data1 (int) msg_id
* @param data2 (int) status_update_serial - must not be used by UI implementations.
*/
#define DC_EVENT_WEBXDC_STATUS_UPDATE 2120
/**
* Message deleted which contained a webxdc instance.
*
* @param data1 (int) msg_id
*/
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
#define DC_EVENT_WEBXDC_STATUS_UPDATE 2120
/**
@@ -5970,38 +5947,28 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages for group name changes.
/// - %1$s will be replaced by the old group name
/// - %2$s will be replaced by the new group name
///
/// @deprecated 2022-09-10
#define DC_STR_MSGGRPNAME 15
/// "Group image changed."
///
/// Used in status messages for group images changes.
///
/// @deprecated 2022-09-10
#define DC_STR_MSGGRPIMGCHANGED 16
/// "Member %1$s added."
///
/// Used in status messages for added members.
/// - %1$s will be replaced by the name of the added member
///
/// @deprecated 2022-09-10
#define DC_STR_MSGADDMEMBER 17
/// "Member %1$s removed."
///
/// Used in status messages for removed members.
/// - %1$s will be replaced by the name of the removed member
///
/// @deprecated 2022-09-10
#define DC_STR_MSGDELMEMBER 18
/// "Group left."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_MSGGROUPLEFT 19
/// "GIF"
@@ -6048,7 +6015,9 @@ void dc_event_unref(dc_event_t* event);
/// - %1$s will be replaced by the subject of the displayed message
#define DC_STR_READRCPT_MAILBODY 32
/// @deprecated Deprecated, this string is no longer needed.
/// "Group image deleted."
///
/// Used in status messages for deleted group images.
#define DC_STR_MSGGRPIMGDELETED 33
/// "End-to-end encryption preferred."
@@ -6101,8 +6070,6 @@ void dc_event_unref(dc_event_t* event);
/// - %1$s will be replaced by an action
/// as #DC_STR_MSGADDMEMBER or #DC_STR_MSGGRPIMGCHANGED (full-stop removed, if any)
/// - %2$s will be replaced by the name of the user taking that action
///
/// @deprecated 2022-09-10
#define DC_STR_MSGACTIONBYUSER 62
/// "%1$s by me"
@@ -6110,8 +6077,6 @@ void dc_event_unref(dc_event_t* event);
/// Used to concretize actions.
/// - %1$s will be replaced by an action
/// as #DC_STR_MSGADDMEMBER or #DC_STR_MSGGRPIMGCHANGED (full-stop removed, if any)
///
/// @deprecated 2022-09-10
#define DC_STR_MSGACTIONBYME 63
/// "Location streaming enabled."
@@ -6175,8 +6140,6 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is disabled."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_DISABLED 75
/// "Message deletion timer is set to %1$s s."
@@ -6184,36 +6147,26 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages when the other constants
/// (#DC_STR_EPHEMERAL_MINUTE, #DC_STR_EPHEMERAL_HOUR and so on) do not match the timer.
/// - %1$s will be replaced by the number of seconds the timer is set to
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_SECONDS 76
/// "Message deletion timer is set to 1 minute."
///
/// Used in status messages.
///
/// @deperecated 2022-09-10
#define DC_STR_EPHEMERAL_MINUTE 77
/// "Message deletion timer is set to 1 hour."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_HOUR 78
/// "Message deletion timer is set to 1 day."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_DAY 79
/// "Message deletion timer is set to 1 week."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_WEEK 80
/// @deprecated Deprecated 2021-01-30, DC_STR_EPHEMERAL_WEEKS is used instead.
@@ -6254,11 +6207,12 @@ void dc_event_unref(dc_event_t* event);
/// "Chat protection enabled."
///
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED 88
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
/// "Chat protection disabled."
///
/// Used in status messages.
#define DC_STR_PROTECTION_DISABLED 89
/// "Reply"
@@ -6280,37 +6234,29 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to %1$s minutes."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_MINUTES and DC_STR_MSG_EPHEMERAL_TIMER_MINUTES_BY.
//
/// `%1$s` will be replaced by the number of minutes (alwasy >1) the timer is set to.
#define DC_STR_EPHEMERAL_MINUTES 93
/// "Message deletion timer is set to %1$s hours."
///
/// Used in status messages.
///
//
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_HOURS and DC_STR_MSG_EPHEMERAL_TIMER_HOURS_BY.
#define DC_STR_EPHEMERAL_HOURS 94
/// "Message deletion timer is set to %1$s days."
///
/// Used in status messages.
///
//
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_DAYS and DC_STR_MSG_EPHEMERAL_TIMER_DAYS_BY.
#define DC_STR_EPHEMERAL_DAYS 95
/// "Message deletion timer is set to %1$s weeks."
///
/// Used in status messages.
///
//
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_WEEKS and DC_STR_MSG_EPHEMERAL_TIMER_WEEKS_BY.
#define DC_STR_EPHEMERAL_WEEKS 96
/// "Forwarded"
@@ -6467,264 +6413,6 @@ void dc_event_unref(dc_event_t* event);
/// Used as status in the connectivity view.
#define DC_STR_NOT_CONNECTED 121
/// "%1$s changed their address from %2$s to %3$s"
///
/// Used as an info message to chats with contacts that changed their address.
#define DC_STR_AEAP_ADDR_CHANGED 122
/// "You changed your email address from %1$s to %2$s.
/// If you now send a message to a group, contacts there will automatically
/// replace the old with your new address.\n\nIt's highly advised to set up
/// your old email provider to forward all emails to your new email address.
/// Otherwise you might miss messages of contacts who did not get your new
/// address yet." + the link to the AEAP blog post
///
/// As soon as there is a post about AEAP, the UIs should add it:
/// set_stock_translation(123, getString(aeap_explanation) + "\n\n" + AEAP_BLOG_LINK)
///
/// Used in a device message that explains AEAP.
#define DC_STR_AEAP_EXPLANATION_AND_LINK 123
/// "You changed group name from \"%1$s\" to \"%2$s\"."
///
/// `%1$s` will be replaced by the old group name.
/// `%2$s` will be replaced by the new group name.
#define DC_STR_GROUP_NAME_CHANGED_BY_YOU 124
/// "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."
///
/// `%1$s` will be replaced by the old group name.
/// `%2$s` will be replaced by the new group name.
/// `%3$s` will be replaced by name and address of the contact who did the action.
#define DC_STR_GROUP_NAME_CHANGED_BY_OTHER 125
/// "You changed the group image."
#define DC_STR_GROUP_IMAGE_CHANGED_BY_YOU 126
/// "Group image changed by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact who did the action.
#define DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER 127
/// "You added member %1$s."
///
/// Used in status messages.
#define DC_STR_ADD_MEMBER_BY_YOU 128
/// "Member %1$s added by %2$s."
///
/// `%1$s` will be replaced by name and address of the contact added to the group.
/// `%2$s` will be replaced by name and address of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_ADD_MEMBER_BY_OTHER 129
/// "You removed member %1$s."
///
/// `%1$s` will be replaced by name and address of the contact removed from the group.
///
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_YOU 130
/// "Member %1$s removed by %2$s."
///
/// `%1$s` will be replaced by name and address of the contact removed from the group.
/// `%2$s` will be replaced by name and address of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
/// "You left the group."
///
/// Used in status messages.
#define DC_STR_GROUP_LEFT_BY_YOU 132
/// "Group left by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_GROUP_LEFT_BY_OTHER 133
/// "You deleted the group image."
///
/// Used in status messages.
#define DC_STR_GROUP_IMAGE_DELETED_BY_YOU 134
/// "Group image deleted by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_GROUP_IMAGE_DELETED_BY_OTHER 135
/// "You enabled location streaming."
///
/// Used in status messages.
#define DC_STR_LOCATION_ENABLED_BY_YOU 136
/// "Location streaming enabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_LOCATION_ENABLED_BY_OTHER 137
/// "You disabled message deletion timer."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU 138
/// "Message deletion timer is disabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER 139
/// "You set message deletion timer to %1$s s."
///
/// `%1$s` will be replaced by the number of seconds (always >1) the timer is set to.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU 140
/// "Message deletion timer is set to %1$s s by %2$s."
///
/// `%1$s` will be replaced by the number of seconds (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER 141
/// "You set message deletion timer to 1 minute."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU 142
/// "Message deletion timer is set to 1 minute by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER 143
/// "You set message deletion timer to 1 hour."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU 144
/// "Message deletion timer is set to 1 hour by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER 145
/// "You set message deletion timer to 1 day."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU 146
/// "Message deletion timer is set to 1 day by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER 147
/// "You set message deletion timer to 1 week."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU 148
/// "Message deletion timer is set to 1 week by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER 149
/// "You set message deletion timer to %1$s minutes."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
#define DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU 150
/// "Message deletion timer is set to %1$s minutes by %2$s."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER 151
/// "You set message deletion timer to %1$s hours."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
#define DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU 152
/// "Message deletion timer is set to %1$s hours by %2$s."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER 153
/// "You set message deletion timer to %1$s days."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
#define DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU 154
/// "Message deletion timer is set to %1$s days by %2$s."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER 155
/// "You set message deletion timer to %1$s weeks."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU 156
/// "Message deletion timer is set to %1$s weeks by %2$s."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You enabled chat protection."
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
/// "Chat protection enabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
/// "You disabled chat protection."
#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
/// "Chat protection disabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
/**
* @}
*/

View File

@@ -169,7 +169,7 @@ pub unsafe extern "C" fn dc_context_unref(context: *mut dc_context_t) {
eprintln!("ignoring careless call to dc_context_unref()");
return;
}
drop(Box::from_raw(context));
Box::from_raw(context);
}
#[no_mangle]
@@ -463,7 +463,7 @@ pub unsafe extern "C" fn dc_event_unref(a: *mut dc_event_t) {
return;
}
drop(Box::from_raw(a));
Box::from_raw(a);
}
#[no_mangle]
@@ -504,7 +504,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::ConnectivityChanged => 2100,
EventType::SelfavatarChanged => 2110,
EventType::WebxdcStatusUpdate { .. } => 2120,
EventType::WebxdcInstanceDeleted { .. } => 2121,
}
}
@@ -551,7 +550,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
contact_id.to_u32() as libc::c_int
}
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
}
}
@@ -583,7 +581,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ImexFileWritten(_)
| EventType::MsgsNoticed(_)
| EventType::ConnectivityChanged
| EventType::WebxdcInstanceDeleted { .. }
| EventType::SelfavatarChanged => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
@@ -640,7 +637,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::WebxdcStatusUpdate { .. }
| EventType::WebxdcInstanceDeleted { .. }
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
@@ -687,7 +683,7 @@ pub unsafe extern "C" fn dc_event_emitter_unref(emitter: *mut dc_event_emitter_t
return;
}
drop(Box::from_raw(emitter));
Box::from_raw(emitter);
}
#[no_mangle]
@@ -1212,11 +1208,6 @@ pub unsafe extern "C" fn dc_get_chat_media(
return ptr::null_mut();
}
let ctx = &*context;
let chat_id = if chat_id == 0 {
None
} else {
Some(ChatId::new(chat_id))
};
let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {}", msg_type));
let or_msg_type2 =
from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {}", or_msg_type2));
@@ -1225,10 +1216,16 @@ pub unsafe extern "C" fn dc_get_chat_media(
block_on(async move {
Box::into_raw(Box::new(
chat::get_chat_media(ctx, chat_id, msg_type, or_msg_type2, or_msg_type3)
.await
.unwrap_or_log_default(ctx, "Failed get_chat_media")
.into(),
chat::get_chat_media(
ctx,
ChatId::new(chat_id),
msg_type,
or_msg_type2,
or_msg_type3,
)
.await
.unwrap_or_log_default(ctx, "Failed get_chat_media")
.into(),
))
})
}
@@ -2426,7 +2423,7 @@ pub unsafe extern "C" fn dc_array_unref(a: *mut dc_array::dc_array_t) {
return;
}
drop(Box::from_raw(a));
Box::from_raw(a);
}
#[no_mangle]
@@ -2607,7 +2604,7 @@ pub unsafe extern "C" fn dc_chatlist_unref(chatlist: *mut dc_chatlist_t) {
eprintln!("ignoring careless call to dc_chatlist_unref()");
return;
}
drop(Box::from_raw(chatlist));
Box::from_raw(chatlist);
}
#[no_mangle]
@@ -2752,7 +2749,7 @@ pub unsafe extern "C" fn dc_chat_unref(chat: *mut dc_chat_t) {
return;
}
drop(Box::from_raw(chat));
Box::from_raw(chat);
}
#[no_mangle]
@@ -2785,16 +2782,6 @@ pub unsafe extern "C" fn dc_chat_get_name(chat: *mut dc_chat_t) -> *mut libc::c_
ffi_chat.chat.get_name().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_mailinglist_addr(chat: *mut dc_chat_t) -> *mut libc::c_char {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_get_mailinglist_addr()");
return "".strdup();
}
let ffi_chat = &*chat;
ffi_chat.chat.get_mailinglist_addr().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut libc::c_char {
if chat.is_null() {
@@ -3022,7 +3009,7 @@ pub unsafe extern "C" fn dc_msg_unref(msg: *mut dc_msg_t) {
return;
}
drop(Box::from_raw(msg));
Box::from_raw(msg);
}
#[no_mangle]
@@ -3744,7 +3731,7 @@ pub unsafe extern "C" fn dc_contact_unref(contact: *mut dc_contact_t) {
eprintln!("ignoring careless call to dc_contact_unref()");
return;
}
drop(Box::from_raw(contact));
Box::from_raw(contact);
}
#[no_mangle]
@@ -3863,16 +3850,6 @@ pub unsafe extern "C" fn dc_contact_get_last_seen(contact: *mut dc_contact_t) ->
ffi_contact.contact.last_seen()
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_was_seen_recently(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_was_seen_recently()");
return 0;
}
let ffi_contact = &*contact;
ffi_contact.contact.was_seen_recently() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
@@ -3908,7 +3885,7 @@ pub unsafe extern "C" fn dc_lot_unref(lot: *mut dc_lot_t) {
return;
}
drop(Box::from_raw(lot));
Box::from_raw(lot);
}
#[no_mangle]
@@ -4200,8 +4177,7 @@ pub unsafe extern "C" fn dc_accounts_get_account(
}
let accounts = &*accounts;
block_on(accounts.read())
.get_account(id)
block_on(async move { accounts.read().await.get_account(id).await })
.map(|ctx| Box::into_raw(Box::new(ctx)))
.unwrap_or_else(std::ptr::null_mut)
}
@@ -4216,8 +4192,7 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
}
let accounts = &*accounts;
block_on(accounts.read())
.get_selected_account()
block_on(async move { accounts.read().await.get_selected_account().await })
.map(|ctx| Box::into_raw(Box::new(ctx)))
.unwrap_or_else(std::ptr::null_mut)
}
@@ -4362,7 +4337,7 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
}
let accounts = &*accounts;
let list = block_on(accounts.read()).get_all();
let list = block_on(async move { accounts.read().await.get_all().await });
let array: dc_array_t = list.into();
Box::into_raw(Box::new(array))
@@ -4422,46 +4397,85 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
block_on(async move { accounts.write().await.maybe_network_lost().await });
}
pub type dc_accounts_event_emitter_t = EventEmitter;
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
accounts: *mut dc_accounts_t,
) -> *mut dc_event_emitter_t {
) -> *mut dc_accounts_event_emitter_t {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_event_emitter()");
return ptr::null_mut();
}
let accounts = &*accounts;
let emitter = block_on(accounts.read()).get_event_emitter();
let emitter = block_on(async move { accounts.read().await.get_event_emitter().await });
Box::into_raw(Box::new(emitter))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_event_emitter_unref(
emitter: *mut dc_accounts_event_emitter_t,
) {
if emitter.is_null() {
eprintln!("ignoring careless call to dc_accounts_event_emitter_unref()");
return;
}
let _ = Box::from_raw(emitter);
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_next_event(
emitter: *mut dc_accounts_event_emitter_t,
) -> *mut dc_event_t {
if emitter.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_next_event()");
return ptr::null_mut();
}
let emitter = &mut *emitter;
block_on(emitter.recv())
.map(|ev| Box::into_raw(Box::new(ev)))
.unwrap_or_else(ptr::null_mut)
}
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use super::*;
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::api::DeltaChatApiV0;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<CommandApi>,
handle: RpcSession<DeltaChatApiV0>,
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_init(
account_manager: *mut dc_accounts_t,
api_version: *const libc::c_char,
) -> *mut dc_jsonrpc_instance_t {
if account_manager.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_init()");
return ptr::null_mut();
}
let api_version = to_string_lossy(api_version);
let cmd_api =
deltachat_jsonrpc::api::CommandApi::from_arc((*account_manager).inner.clone());
let rpc_api = match api_version.as_str() {
"v0" => {
deltachat_jsonrpc::api::DeltaChatApiV0::from_arc((*account_manager).inner.clone())
}
version => {
eprintln!(
"Error initializing JSON-RPC API: API version {} is not supported.",
version
);
return ptr::null_mut();
}
};
let (request_handle, receiver) = RpcClient::new();
let handle = RpcSession::new(request_handle, cmd_api);
let handle = RpcSession::new(request_handle, rpc_api);
let instance = dc_jsonrpc_instance_t { receiver, handle };

View File

@@ -51,14 +51,13 @@ impl Lot {
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
Qr::Account { domain } => Some(domain),
Qr::WebrtcInstance { domain, .. } => Some(domain),
Qr::Addr { draft, .. } => draft.as_deref(),
Qr::Addr { .. } => None,
Qr::Url { url } => Some(url),
Qr::Text { text } => Some(text),
Qr::WithdrawVerifyContact { .. } => None,
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
Qr::ReviveVerifyContact { .. } => None,
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
Qr::Login { address, .. } => Some(address),
},
Self::Error(err) => Some(err),
}
@@ -80,13 +79,7 @@ impl Lot {
Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username,
Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self,
},
Self::Qr(qr) => match qr {
Qr::Addr {
draft: Some(_draft),
..
} => Meaning::Text1Draft,
_ => Meaning::None,
},
Self::Qr(_qr) => Meaning::None,
Self::Error(_err) => Meaning::None,
}
}
@@ -109,7 +102,6 @@ impl Lot {
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
Qr::Login { .. } => LotState::QrLogin,
},
Self::Error(_err) => LotState::QrError,
}
@@ -126,14 +118,13 @@ impl Lot {
Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
Qr::Addr { contact_id } => contact_id.to_u32(),
Qr::Url { .. } => Default::default(),
Qr::Text { .. } => Default::default(),
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::WithdrawVerifyGroup { .. } => Default::default(),
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::ReviveVerifyGroup { .. } => Default::default(),
Qr::Login { .. } => Default::default(),
},
Self::Error(_) => Default::default(),
}
@@ -198,9 +189,6 @@ pub enum LotState {
/// text1=groupname
QrReviveVerifyGroup = 512,
/// text1=email_address
QrLogin = 520,
// Message States
MsgInFresh = 10,
MsgInNoticed = 13,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.96.0"
version = "1.86.0"
description = "DeltaChat JSON-RPC API"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
@@ -20,8 +20,8 @@ serde = { version = "1.0", features = ["derive"] }
tempfile = "3.3.0"
log = "0.4"
async-channel = { version = "1.6.1" }
futures = { version = "0.3.24" }
serde_json = "1.0.85"
futures = { version = "0.3.19" }
serde_json = "1.0.75"
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
tokio = { version = "1.19.2" }

View File

@@ -60,7 +60,6 @@ pub fn event_to_json_rpc_notification(event: Event) -> Value {
msg_id,
status_update_serial,
} => (json!(msg_id), json!(status_update_serial)),
EventType::WebxdcInstanceDeleted { msg_id } => (json!(msg_id), Value::Null),
};
let id: EventTypeName = event.typ.into();
@@ -103,7 +102,6 @@ pub enum EventTypeName {
ConnectivityChanged,
SelfavatarChanged,
WebxdcStatusUpdate,
WebXdInstanceDeleted,
}
impl From<EventType> for EventTypeName {
@@ -139,7 +137,6 @@ impl From<EventType> for EventTypeName {
EventType::ConnectivityChanged => ConnectivityChanged,
EventType::SelfavatarChanged => SelfavatarChanged,
EventType::WebxdcStatusUpdate { .. } => WebxdcStatusUpdate,
EventType::WebxdcInstanceDeleted { .. } => WebXdInstanceDeleted,
}
}
}

View File

@@ -1,18 +1,13 @@
use anyhow::{anyhow, bail, Context, Result};
use deltachat::{
chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, marknoticed_chat,
remove_contact_from_chat, Chat, ChatId, ChatItem,
},
chat::{get_chat_media, get_chat_msgs, ChatId},
chatlist::Chatlist,
config::Config,
contact::{may_be_valid_addr, Contact, ContactId},
context::get_info,
message::{delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype},
message::{Message, MsgId, Viewtype},
provider::get_provider_info,
qr,
qr_code_generator::get_securejoin_qr_svg,
securejoin,
webxdc::StatusUpdateSerial,
};
use std::collections::BTreeMap;
@@ -27,7 +22,6 @@ pub mod events;
pub mod types;
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::QrObject;
use types::account::Account;
use types::chat::FullChat;
@@ -37,26 +31,23 @@ use types::message::MessageObject;
use types::provider_info::ProviderInfo;
use types::webxdc::WebxdcMessageInfo;
use self::types::{
chat::{BasicChat, MuteDuration},
message::{MessageNotificationInfo, MessageViewtype},
};
use self::types::message::MessageViewtype;
#[derive(Clone, Debug)]
pub struct CommandApi {
pub struct DeltaChatApiV0 {
pub(crate) accounts: Arc<RwLock<Accounts>>,
}
impl CommandApi {
impl DeltaChatApiV0 {
pub fn new(accounts: Accounts) -> Self {
CommandApi {
DeltaChatApiV0 {
accounts: Arc::new(RwLock::new(accounts)),
}
}
#[allow(dead_code)]
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
CommandApi { accounts }
DeltaChatApiV0 { accounts }
}
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
@@ -65,13 +56,14 @@ impl CommandApi {
.read()
.await
.get_account(id)
.await
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
Ok(sc)
}
}
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl CommandApi {
impl DeltaChatApiV0 {
// ---------------------------------------------
// Misc top level functions
// ---------------------------------------------
@@ -99,7 +91,7 @@ impl CommandApi {
}
async fn get_all_account_ids(&self) -> Vec<u32> {
self.accounts.read().await.get_all()
self.accounts.read().await.get_all().await
}
/// Select account id for internally selected state.
@@ -111,14 +103,14 @@ impl CommandApi {
/// Get the selected account id of the internal state..
/// TODO: Likely this is deprecated as all methods take an account id now.
async fn get_selected_account_id(&self) -> Option<u32> {
self.accounts.read().await.get_selected_account_id()
self.accounts.read().await.get_selected_account_id().await
}
/// Get a list of all configured accounts.
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
let mut accounts = Vec::new();
for id in self.accounts.read().await.get_all() {
let context_option = self.accounts.read().await.get_account(id);
for id in self.accounts.read().await.get_all().await {
let context_option = self.accounts.read().await.get_account(id).await;
if let Some(ctx) = context_option {
accounts.push(Account::from_context(&ctx, id).await?)
} else {
@@ -134,7 +126,7 @@ impl CommandApi {
/// Get top-level info for an account.
async fn get_account_info(&self, account_id: u32) -> Result<Account> {
let context_option = self.accounts.read().await.get_account(account_id);
let context_option = self.accounts.read().await.get_account(account_id).await;
if let Some(ctx) = context_option {
Ok(Account::from_context(&ctx, account_id).await?)
} else {
@@ -199,22 +191,17 @@ impl CommandApi {
}
/// Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
/// Before this function is called, `checkQr()` should confirm the type of the
/// QR code is `account` or `webrtcInstance`.
/// Before this function is called, dc_check_qr() should confirm the type of the
/// QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
///
/// Internally, the function will call dc_set_config() with the appropriate keys,
/// e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
/// or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
async fn set_config_from_qr(&self, account_id: u32, qr_content: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
qr::set_config_from_qr(&ctx, &qr_content).await
}
async fn check_qr(&self, account_id: u32, qr_content: String) -> Result<QrObject> {
let ctx = self.get_context(account_id).await?;
let qr = qr::check_qr(&ctx, &qr_content).await?;
let qr_object = QrObject::from(qr);
Ok(qr_object)
}
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
get_config(&ctx, &key).await
@@ -373,13 +360,6 @@ impl CommandApi {
FullChat::try_from_dc_chat_id(&ctx, chat_id).await
}
/// get basic info about a chat,
/// use chatlist_get_full_chat_by_id() instead if you need more information
async fn get_basic_chat_info(&self, account_id: u32, chat_id: u32) -> Result<BasicChat> {
let ctx = self.get_context(account_id).await?;
BasicChat::try_from_dc_chat_id(&ctx, chat_id).await
}
async fn accept_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).accept(&ctx).await
@@ -390,110 +370,6 @@ impl CommandApi {
ChatId::new(chat_id).block(&ctx).await
}
/// Delete a chat.
///
/// Messages are deleted from the device and the chat database entry is deleted.
/// After that, the event #DC_EVENT_MSGS_CHANGED is posted.
///
/// Things that are _not done_ implicitly:
///
/// - Messages are **not deleted from the server**.
/// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
/// and the user may create the chat again.
/// - **Groups are not left** - this would
/// be unexpected as (1) deleting a normal chat also does not prevent new mails
/// from arriving, (2) leaving a group requires sending a message to
/// all group members - especially for groups not used for a longer time, this is
/// really unexpected when deletion results in contacting all members again,
/// (3) only leaving groups is also a valid usecase.
///
/// To leave a chat explicitly, use dc_remove_contact_from_chat() with
/// chat_id=DC_CONTACT_ID_SELF)
// TODO fix doc comment after adding dc_remove_contact_from_chat
async fn delete_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).delete(&ctx).await
}
/// Get encryption info for a chat.
/// Get a multi-line encryption info, containing encryption preferences of all members.
/// Can be used to find out why messages sent to group are not encrypted.
///
/// returns Multi-line text
async fn get_chat_encryption_info(&self, account_id: u32, chat_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).get_encryption_info(&ctx).await
}
/// Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
/// The QR code is compatible to the OPENPGP4FPR format
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
///
/// The scanning device will pass the scanned content to `checkQr()` then;
/// if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
/// an out-of-band-verification can be joined using dc_join_securejoin()
///
/// chat_id: If set to a group-chat-id,
/// 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
/// for details about both protocols.
///
/// return format: `[code, svg]`
// TODO fix doc comment after adding dc_join_securejoin
async fn get_chat_securejoin_qr_code_svg(
&self,
account_id: u32,
chat_id: Option<u32>,
) -> Result<(String, String)> {
let ctx = self.get_context(account_id).await?;
let chat = chat_id.map(ChatId::new);
Ok((
securejoin::get_securejoin_qr(&ctx, chat).await?,
get_securejoin_qr_svg(&ctx, chat).await?,
))
}
async fn leave_group(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
remove_contact_from_chat(&ctx, ChatId::new(chat_id), ContactId::SELF).await
}
/// Remove a member from a group.
///
/// If the group is already _promoted_ (any message was sent to the group),
/// all group members are informed by a special status message that is sent automatically by this function.
///
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
async fn remove_contact_from_chat(
&self,
account_id: u32,
chat_id: u32,
contact_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
remove_contact_from_chat(&ctx, ChatId::new(chat_id), ContactId::new(contact_id)).await
}
/// Add a member to a group.
///
/// If the group is already _promoted_ (any message was sent to the group),
/// all group members are informed by a special status message that is sent automatically by this function.
///
/// If the group has group protection enabled, only verified contacts can be added to the group.
///
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
async fn add_contact_to_chat(
&self,
account_id: u32,
chat_id: u32,
contact_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
add_contact_to_chat(&ctx, ChatId::new(chat_id), ContactId::new(contact_id)).await
}
// for now only text messages, because we only used text messages in desktop thusfar
async fn add_device_message(
&self,
@@ -509,101 +385,10 @@ impl CommandApi {
Ok(message_id.to_u32())
}
/// Mark all messages in a chat as _noticed_.
/// _Noticed_ messages are no longer _fresh_ and do not count as being unseen
/// but are still waiting for being marked as "seen" using markseen_msgs()
/// (IMAP/MDNs is not done for noticed messages).
///
/// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
/// See also markseen_msgs().
async fn marknoticed_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
marknoticed_chat(&ctx, ChatId::new(chat_id)).await
}
async fn get_first_unread_message_of_chat(
&self,
account_id: u32,
chat_id: u32,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
// TODO: implement this in core with an SQL query, that will be way faster
let messages = get_chat_msgs(&ctx, ChatId::new(chat_id), 0).await?;
let mut first_unread_message_id = None;
for item in messages.into_iter().rev() {
if let ChatItem::Message { msg_id } = item {
match msg_id.get_state(&ctx).await? {
MessageState::InSeen => break,
MessageState::InFresh | MessageState::InNoticed => {
first_unread_message_id = Some(msg_id)
}
_ => continue,
}
}
}
Ok(first_unread_message_id.map(|id| id.to_u32()))
}
/// Set mute duration of a chat.
///
/// The UI can then call is_chat_muted() when receiving a new message
/// to decide whether it should trigger an notification.
///
/// Muted chats should not sound or vibrate
/// and should not show a visual notification in the system area.
/// Moreover, muted chats should be excluded from global badge counter
/// (get_fresh_msgs() skips muted chats therefore)
/// and the in-app, per-chat badge counter should use a less obtrusive color.
///
/// Sends out #DC_EVENT_CHAT_MODIFIED.
async fn set_chat_mute_duration(
&self,
account_id: u32,
chat_id: u32,
duration: MuteDuration,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
chat::set_muted(&ctx, ChatId::new(chat_id), duration.try_into_core_type()?).await
}
/// Check whether the chat is currently muted (can be changed by set_chat_mute_duration()).
///
/// This is available as a standalone function outside of fullchat, because it might be only needed for notification
async fn is_chat_muted(&self, account_id: u32, chat_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
Ok(Chat::load_from_db(&ctx, ChatId::new(chat_id))
.await?
.is_muted())
}
// ---------------------------------------------
// message list
// ---------------------------------------------
/// Mark messages as presented to the user.
/// Typically, UIs call this function on scrolling through the message list,
/// when the messages are presented at least for a little moment.
/// The concrete action depends on the type of the chat and on the users settings
/// (dc_msgs_presented() may be a better name therefore, but well. :)
///
/// - For normal chats, the IMAP state is updated, MDN is sent
/// (if set_config()-options `mdns_enabled` is set)
/// and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
///
/// - For contact requests, no IMAP or MDNs is done
/// and the internal state is not changed therefore.
/// See also marknoticed_chat().
///
/// Moreover, timer is started for incoming ephemeral messages.
/// This also happens for contact requests chats.
///
/// One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
async fn markseen_msgs(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
}
async fn message_list_get_message_ids(
&self,
account_id: u32,
@@ -642,34 +427,6 @@ impl CommandApi {
Ok(messages)
}
/// Fetch info desktop needs for creating a notification for a message
async fn message_get_notification_info(
&self,
account_id: u32,
message_id: u32,
) -> Result<MessageNotificationInfo> {
let ctx = self.get_context(account_id).await?;
MessageNotificationInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
}
/// Delete messages. The messages are deleted on the current device and
/// on the IMAP server.
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
delete_msgs(&ctx, &msgs).await
}
/// Get an informational text for a single message. The text is multiline and may
/// contain e.g. the raw text of the message.
///
/// The max. text returned is typically longer (about 100000 characters) than the
/// max. text returned by dc_msg_get_text() (about 30000 characters).
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
get_msg_info(&ctx, MsgId::new(message_id)).await
}
// ---------------------------------------------
// contact
// ---------------------------------------------
@@ -802,19 +559,6 @@ impl CommandApi {
}
Ok(contacts)
}
/// Get encryption info for a contact.
/// Get a multi-line encryption info, containing your fingerprint and the
/// fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
async fn get_contact_encryption_info(
&self,
account_id: u32,
contact_id: u32,
) -> Result<String> {
let ctx = self.get_context(account_id).await?;
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
}
// ---------------------------------------------
// chat
// ---------------------------------------------
@@ -825,120 +569,31 @@ impl CommandApi {
/// The list is already sorted and starts with the oldest message.
/// Clients should not try to re-sort the list as this would be an expensive action
/// and would result in inconsistencies between clients.
///
/// Setting `chat_id` to `None` (`null` in typescript) means get messages with media
/// from any chat of the currently used account.
async fn chat_get_media(
&self,
account_id: u32,
chat_id: Option<u32>,
chat_id: u32,
message_type: MessageViewtype,
or_message_type2: Option<MessageViewtype>,
or_message_type3: Option<MessageViewtype>,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let chat_id = match chat_id {
None | Some(0) => None,
Some(id) => Some(ChatId::new(id)),
};
let msg_type = message_type.into();
let or_msg_type2 = or_message_type2.map_or(Viewtype::Unknown, |v| v.into());
let or_msg_type3 = or_message_type3.map_or(Viewtype::Unknown, |v| v.into());
let media = get_chat_media(&ctx, chat_id, msg_type, or_msg_type2, or_msg_type3).await?;
let media = get_chat_media(
&ctx,
ChatId::new(chat_id),
msg_type,
or_msg_type2,
or_msg_type3,
)
.await?;
Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect())
}
/// Search next/previous message based on a given message and a list of types.
/// Typically used to implement the "next" and "previous" buttons
/// in a gallery or in a media player.
///
/// one combined call for getting chat::get_next_media for both directions
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
async fn chat_get_neighboring_media(
&self,
account_id: u32,
msg_id: u32,
message_type: MessageViewtype,
or_message_type2: Option<MessageViewtype>,
or_message_type3: Option<MessageViewtype>,
) -> Result<(Option<u32>, Option<u32>)> {
let ctx = self.get_context(account_id).await?;
let msg_type: Viewtype = message_type.into();
let msg_type2: Viewtype = or_message_type2.map(|v| v.into()).unwrap_or_default();
let msg_type3: Viewtype = or_message_type3.map(|v| v.into()).unwrap_or_default();
let prev = chat::get_next_media(
&ctx,
MsgId::new(msg_id),
chat::Direction::Backward,
msg_type,
msg_type2,
msg_type3,
)
.await?
.map(|id| id.to_u32());
let next = chat::get_next_media(
&ctx,
MsgId::new(msg_id),
chat::Direction::Forward,
msg_type,
msg_type2,
msg_type3,
)
.await?
.map(|id| id.to_u32());
Ok((prev, next))
}
// ---------------------------------------------
// connectivity
// ---------------------------------------------
/// Indicate that the network likely has come back.
/// or just that the network conditions might have changed
async fn maybe_network(&self) -> Result<()> {
self.accounts.read().await.maybe_network().await;
Ok(())
}
/// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
/// One of:
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
/// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
/// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
/// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
///
/// We don't use exact values but ranges here so that we can split up
/// states into multiple states in the future.
///
/// Meant as a rough overview that can be shown
/// e.g. in the title of the main screen.
///
/// If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
async fn get_connectivity(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.get_connectivity().await as u32)
}
/// Get an overview of the current connectivity, and possibly more statistics.
/// Meant to give the user more insight about the current status than
/// the basic connectivity info returned by get_connectivity(); show this
/// e.g., if the user taps on said basic connectivity info.
///
/// If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
///
/// This comes as an HTML from the core so that we can easily improve it
/// and the improvement instantly reaches all UIs.
async fn get_connectivity_html(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
ctx.get_connectivity_html().await
}
// ---------------------------------------------
// webxdc
// ---------------------------------------------
@@ -979,45 +634,6 @@ impl CommandApi {
WebxdcMessageInfo::get_for_message(&ctx, MsgId::new(instance_msg_id)).await
}
/// Forward messages to another chat.
///
/// All types of messages can be forwarded,
/// however, they will be flagged as such (dc_msg_is_forwarded() is set).
///
/// Original sender, info-state and webxdc updates are not forwarded on purpose.
async fn forward_messages(
&self,
account_id: u32,
message_ids: Vec<u32>,
chat_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
}
// ---------------------------------------------
// functions for the composer
// the composer is the message input field
// ---------------------------------------------
async fn remove_draft(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).set_draft(&ctx, None).await
}
/// Get draft for a chat, if any.
async fn get_draft(&self, account_id: u32, chat_id: u32) -> Result<Option<MessageObject>> {
let ctx = self.get_context(account_id).await?;
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
Ok(Some(
MessageObject::from_msg_id(&ctx, draft.get_id()).await?,
))
} else {
Ok(None)
}
}
// ---------------------------------------------
// misc prototyping functions
// that might get removed later again
@@ -1038,91 +654,6 @@ impl CommandApi {
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
}
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
// the better version will just be sending the current draft, though there will be probably something similar with more options to this for the corner cases like setting a marker on the map
async fn misc_send_msg(
&self,
account_id: u32,
chat_id: u32,
text: Option<String>,
file: Option<String>,
location: Option<(f64, f64)>,
quoted_message_id: Option<u32>,
) -> Result<(u32, MessageObject)> {
let ctx = self.get_context(account_id).await?;
let mut message = Message::new(if file.is_some() {
Viewtype::File
} else {
Viewtype::Text
});
if text.is_some() {
message.set_text(text);
}
if let Some(file) = file {
message.set_file(file, None);
}
if let Some((latitude, longitude)) = location {
message.set_location(latitude, longitude);
}
if let Some(id) = quoted_message_id {
message
.set_quote(
&ctx,
Some(
&Message::load_from_db(&ctx, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
}
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
.await?
.to_u32();
let message = MessageObject::from_message_id(&ctx, msg_id).await?;
Ok((msg_id, message))
}
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
// the better version should support:
// - changing viewtype to enable/disable compression
// - keeping same message id as long as attachment does not change for webxdc messages
async fn misc_set_draft(
&self,
account_id: u32,
chat_id: u32,
text: Option<String>,
file: Option<String>,
quoted_message_id: Option<u32>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let mut draft = Message::new(if file.is_some() {
Viewtype::File
} else {
Viewtype::Text
});
if text.is_some() {
draft.set_text(text);
}
if let Some(file) = file {
draft.set_file(file, None);
}
if let Some(id) = quoted_message_id {
draft
.set_quote(
&ctx,
Some(
&Message::load_from_db(&ctx, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
}
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
}
}
// Helper functions (to prevent code duplication)
@@ -1132,19 +663,11 @@ async fn set_config(
value: Option<&str>,
) -> Result<(), anyhow::Error> {
if key.starts_with("ui.") {
ctx.set_ui_config(key, value).await?;
ctx.set_ui_config(key, value).await
} else {
ctx.set_config(Config::from_str(key).context("unknown key")?, value)
.await?;
match key {
"sentbox_watch" | "mvbox_move" | "only_fetch_mvbox" => {
ctx.restart_io_if_running().await;
}
_ => {}
}
.await
}
Ok(())
}
async fn get_config(

View File

@@ -1,13 +1,10 @@
use std::time::{Duration, SystemTime};
use anyhow::{anyhow, bail, Result};
use deltachat::chat::{self, get_chat_contacts};
use anyhow::{anyhow, Result};
use deltachat::chat::get_chat_contacts;
use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
use deltachat::contact::{Contact, ContactId};
use deltachat::context::Context;
use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
@@ -36,8 +33,6 @@ pub struct FullChat {
is_muted: bool,
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
can_send: bool,
was_seen_recently: bool,
mailing_list_address: String,
}
impl FullChat {
@@ -70,25 +65,12 @@ impl FullChat {
let can_send = chat.can_send(context).await?;
let was_seen_recently = if chat.get_type() == Chattype::Single {
match contact_ids.get(0) {
Some(contact) => Contact::load_from_db(context, *contact)
.await?
.was_seen_recently(),
None => false,
}
} else {
false
};
let mailing_list_address = chat.get_mailinglist_addr().to_string();
Ok(FullChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
archived: chat.get_visibility() == deltachat::chat::ChatVisibility::Archived,
chat_type: chat
.get_type()
.to_u32()
@@ -105,91 +87,6 @@ impl FullChat {
is_muted: chat.is_muted(),
ephemeral_timer,
can_send,
was_seen_recently,
mailing_list_address,
})
}
}
/// cheaper version of fullchat, omits:
/// - contacts
/// - contact_ids
/// - fresh_message_counter
/// - ephemeral_timer
/// - self_in_group
/// - was_seen_recently
/// - can_send
///
/// used when you only need the basic metadata of a chat like type, name, profile picture
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct BasicChat {
id: u32,
name: String,
is_protected: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
chat_type: u32,
is_unpromoted: bool,
is_self_talk: bool,
color: String,
is_contact_request: bool,
is_device_chat: bool,
is_muted: bool,
}
impl BasicChat {
pub async fn try_from_dc_chat_id(context: &Context, chat_id: u32) -> Result<Self> {
let rust_chat_id = ChatId::new(chat_id);
let chat = Chat::load_from_db(context, rust_chat_id).await?;
let profile_image = match chat.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let color = color_int_to_hex_string(chat.get_color(context).await?);
Ok(BasicChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
chat_type: chat
.get_type()
.to_u32()
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
color,
is_contact_request: chat.is_contact_request(),
is_device_chat: chat.is_device_talk(),
is_muted: chat.is_muted(),
})
}
}
#[derive(Clone, Serialize, Deserialize, TypeDef)]
pub enum MuteDuration {
NotMuted,
Forever,
Until(i64),
}
impl MuteDuration {
pub fn try_into_core_type(self) -> Result<chat::MuteDuration> {
match self {
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
MuteDuration::Until(n) => {
if n <= 0 {
bail!("failed to read mute duration")
}
Ok(SystemTime::now()
.checked_add(Duration::from_secs(n as u64))
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
}
}
}
}

View File

@@ -1,6 +1,6 @@
use anyhow::Result;
use deltachat::constants::*;
use deltachat::contact::{Contact, ContactId};
use deltachat::contact::ContactId;
use deltachat::{
chat::{get_chat_contacts, ChatVisibility},
chatlist::Chatlist,
@@ -42,11 +42,8 @@ pub enum ChatListItemFetchResult {
is_pinned: bool,
is_muted: bool,
is_contact_request: bool,
/// true when chat is a broadcastlist
is_broadcast: bool,
/// contact id if this is a dm chat (for view profile entry in context menu)
dm_chat_contact: Option<u32>,
was_seen_recently: bool,
},
ArchiveLink,
#[serde(rename_all = "camelCase")]
@@ -95,20 +92,10 @@ pub(crate) async fn get_chat_list_item_by_id(
let self_in_group = chat_contacts.contains(&ContactId::SELF);
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
let contact = chat_contacts.get(0);
let was_seen_recently = match contact {
Some(contact) => Contact::load_from_db(ctx, *contact)
.await?
.was_seen_recently(),
None => false,
};
(
contact.map(|contact_id| contact_id.to_u32()),
was_seen_recently,
)
let dm_chat_contact = if chat.get_type() == Chattype::Single {
chat_contacts.get(0).map(|contact_id| contact_id.to_u32())
} else {
(None, false)
None
};
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
@@ -134,8 +121,6 @@ pub(crate) async fn get_chat_list_item_by_id(
is_pinned: visibility == ChatVisibility::Pinned,
is_muted: chat.is_muted(),
is_contact_request: chat.is_contact_request(),
is_broadcast: chat.get_type() == Chattype::Broadcast,
dm_chat_contact,
was_seen_recently,
})
}

View File

@@ -20,9 +20,6 @@ pub struct ContactObject {
name_and_addr: String,
is_blocked: bool,
is_verified: bool,
/// the contact's last seen timestamp
last_seen: i64,
was_seen_recently: bool,
}
impl ContactObject {
@@ -48,8 +45,6 @@ impl ContactObject {
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
is_verified,
last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(),
})
}
}

View File

@@ -1,8 +1,6 @@
use anyhow::{anyhow, Result};
use deltachat::chat::Chat;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::download;
use deltachat::message::Message;
use deltachat::message::MsgId;
use deltachat::message::Viewtype;
@@ -11,9 +9,7 @@ use serde::Deserialize;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Message", rename_all = "camelCase")]
@@ -21,9 +17,8 @@ pub struct MessageObject {
id: u32,
chat_id: u32,
from_id: u32,
quote: Option<MessageQuote>,
parent_id: Option<u32>,
quoted_text: Option<String>,
quoted_message_id: Option<u32>,
text: Option<String>,
has_location: bool,
has_html: bool,
@@ -58,89 +53,29 @@ pub struct MessageObject {
file_mime: Option<String>,
file_bytes: u64,
file_name: Option<String>,
webxdc_info: Option<WebxdcMessageInfo>,
download_state: DownloadState,
}
#[derive(Serialize, TypeDef)]
#[serde(tag = "kind")]
enum MessageQuote {
JustText {
text: String,
},
#[serde(rename_all = "camelCase")]
WithMessage {
text: String,
message_id: u32,
author_display_name: String,
author_display_color: String,
override_sender_name: Option<String>,
image: Option<String>,
is_forwarded: bool,
},
}
impl MessageObject {
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
let msg_id = MsgId::new(message_id);
Self::from_msg_id(context, msg_id).await
}
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let quoted_message_id = message
.quoted_message(context)
.await?
.map(|m| m.get_id().to_u32());
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
let file_bytes = message.get_filebytes(context).await;
let override_sender_name = message.get_override_sender_name();
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
} else {
None
};
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
let download_state = message.download_state().into();
let quote = if let Some(quoted_text) = message.quoted_text() {
match message.quoted_message(context).await? {
Some(quote) => {
let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
Some(MessageQuote::WithMessage {
text: quoted_text,
message_id: quote.get_id().to_u32(),
author_display_name: quote_author.get_display_name().to_owned(),
author_display_color: color_int_to_hex_string(quote_author.get_color()),
override_sender_name: quote.get_override_sender_name(),
image: if quote.get_viewtype() == Viewtype::Image
|| quote.get_viewtype() == Viewtype::Gif
{
match quote.get_file(context) {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
}
} else {
None
},
is_forwarded: quote.is_forwarded(),
})
}
None => Some(MessageQuote::JustText { text: quoted_text }),
}
} else {
None
};
Ok(MessageObject {
id: msg_id.to_u32(),
id: message_id,
chat_id: message.get_chat_id().to_u32(),
from_id: message.get_from_id().to_u32(),
quote,
parent_id,
quoted_text: message.quoted_text(),
quoted_message_id,
text: message.get_text(),
has_location: message.has_location(),
has_html: message.has_html(),
@@ -186,9 +121,6 @@ impl MessageObject {
file_mime: message.get_filemime(),
file_bytes,
file_name: message.get_filename(),
webxdc_info,
download_state,
})
}
}
@@ -268,80 +200,3 @@ impl From<MessageViewtype> for Viewtype {
}
}
}
#[derive(Serialize, TypeDef)]
pub enum DownloadState {
Done,
Available,
Failure,
InProgress,
}
impl From<download::DownloadState> for DownloadState {
fn from(state: download::DownloadState) -> Self {
match state {
download::DownloadState::Done => DownloadState::Done,
download::DownloadState::Available => DownloadState::Available,
download::DownloadState::Failure => DownloadState::Failure,
download::DownloadState::InProgress => DownloadState::InProgress,
}
}
}
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct MessageNotificationInfo {
id: u32,
chat_id: u32,
account_id: u32,
image: Option<String>,
image_mime_type: Option<String>,
chat_name: String,
chat_profile_image: Option<String>,
/// also known as summary_text1
summary_prefix: Option<String>,
/// also known as summary_text2
summary_text: String,
}
impl MessageNotificationInfo {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
let image = if matches!(
message.get_viewtype(),
Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
) {
message
.get_file(context)
.map(|path_buf| path_buf.to_str().map(|s| s.to_owned()))
.unwrap_or_default()
} else {
None
};
let chat_profile_image = chat
.get_profile_image(context)
.await?
.map(|path_buf| path_buf.to_str().map(|s| s.to_owned()))
.unwrap_or_default();
let summary = message.get_summary(context, Some(&chat)).await?;
Ok(MessageNotificationInfo {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
account_id: context.get_id(),
image,
image_mime_type: message.get_filemime(),
chat_name: chat.name,
chat_profile_image,
summary_prefix: summary.prefix.map(|s| s.to_string()),
summary_text: summary.text,
})
}
}

View File

@@ -1,7 +1,3 @@
use deltachat::qr::Qr;
use serde::Serialize;
use typescript_type_def::TypeDef;
pub mod account;
pub mod chat;
pub mod chat_list;
@@ -21,213 +17,3 @@ fn maybe_empty_string_to_option(string: String) -> Option<String> {
Some(string)
}
}
#[derive(Serialize, TypeDef)]
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
AskVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
FprOk {
contact_id: u32,
},
FprMismatch {
contact_id: Option<u32>,
},
FprWithoutAddr {
fingerprint: String,
},
Account {
domain: String,
},
WebrtcInstance {
domain: String,
instance_pattern: String,
},
Addr {
contact_id: u32,
draft: Option<String>,
},
Url {
url: String,
},
Text {
text: String,
},
WithdrawVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
WithdrawVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
Login {
address: String,
},
}
impl From<Qr> for QrObject {
fn from(qr: Qr) -> Self {
match qr {
Qr::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::FprOk { contact_id } => {
let contact_id = contact_id.to_u32();
QrObject::FprOk { contact_id }
}
Qr::FprMismatch { contact_id } => {
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
QrObject::FprMismatch { contact_id }
}
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain },
Qr::WebrtcInstance {
domain,
instance_pattern,
} => QrObject::WebrtcInstance {
domain,
instance_pattern,
},
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft }
}
Qr::Url { url } => QrObject::Url { url },
Qr::Text { text } => QrObject::Text { text },
Qr::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::Login { address, .. } => QrObject::Login { address },
}
}
}

View File

@@ -33,8 +33,6 @@ pub struct WebxdcMessageInfo {
/// defaults to an empty string.
/// Implementations may offer an menu or a button to open this URL.
source_code_url: Option<String>,
/// True if full internet access should be granted to the app.
internet_access: bool,
}
impl WebxdcMessageInfo {
@@ -49,7 +47,6 @@ impl WebxdcMessageInfo {
document,
summary,
source_code_url,
internet_access,
} = message.get_webxdc_info(context).await?;
Ok(Self {
@@ -58,7 +55,6 @@ impl WebxdcMessageInfo {
document: maybe_empty_string_to_option(document),
summary: maybe_empty_string_to_option(summary),
source_code_url: maybe_empty_string_to_option(source_code_url),
internet_access,
})
}
}

View File

@@ -1,10 +1,11 @@
pub mod api;
pub use api::events;
pub use api::{Accounts, DeltaChatApiV0};
pub use yerpc;
#[cfg(test)]
mod tests {
use super::api::{Accounts, CommandApi};
use super::api::{Accounts, DeltaChatApiV0};
use async_channel::unbounded;
use futures::StreamExt;
use tempfile::TempDir;
@@ -14,7 +15,7 @@ mod tests {
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let accounts = Accounts::new(tmp_dir).await?;
let api = CommandApi::new(accounts);
let api = DeltaChatApiV0::new(accounts);
let (sender, mut receiver) = unbounded::<String>();
@@ -55,7 +56,7 @@ mod tests {
async fn test_batch_set_config() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let accounts = Accounts::new(tmp_dir).await?;
let api = CommandApi::new(accounts);
let api = DeltaChatApiV0::new(accounts);
let (sender, mut receiver) = unbounded::<String>();

View File

@@ -6,7 +6,7 @@ use yerpc::{RpcClient, RpcSession};
mod api;
use api::events::event_to_json_rpc_notification;
use api::{Accounts, CommandApi};
use api::{Accounts, DeltaChatApiV0};
const DEFAULT_PORT: u16 = 20808;
@@ -20,10 +20,10 @@ async fn main() -> Result<(), std::io::Error> {
.unwrap_or(DEFAULT_PORT);
log::info!("Starting with accounts directory `{path}`.");
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
let state = CommandApi::new(accounts);
let state = DeltaChatApiV0::new(accounts);
let app = Router::new()
.route("/ws", get(handler))
.route("/rpc/v0", get(handler))
.layer(Extension(state.clone()));
tokio::spawn(async move {
@@ -40,11 +40,11 @@ async fn main() -> Result<(), std::io::Error> {
Ok(())
}
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<DeltaChatApiV0>) -> Response {
let (client, out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), api.clone());
tokio::spawn(async move {
let events = api.accounts.read().await.get_event_emitter();
let events = api.accounts.read().await.get_event_emitter().await;
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
client.send_notification("event", Some(event)).await.ok();

View File

@@ -111,21 +111,18 @@ export class RawClient {
/**
* Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
* Before this function is called, `checkQr()` should confirm the type of the
* QR code is `account` or `webrtcInstance`.
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
*
* Internally, the function will call dc_set_config() with the appropriate keys,
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
*/
public setConfigFromQr(accountId: T.U32, qrContent: string): Promise<null> {
return (this._transport.request('set_config_from_qr', [accountId, qrContent] as RPC.Params)) as Promise<null>;
}
public checkQr(accountId: T.U32, qrContent: string): Promise<T.Qr> {
return (this._transport.request('check_qr', [accountId, qrContent] as RPC.Params)) as Promise<T.Qr>;
}
public getConfig(accountId: T.U32, key: string): Promise<(string|null)> {
return (this._transport.request('get_config', [accountId, key] as RPC.Params)) as Promise<(string|null)>;
}
@@ -204,14 +201,6 @@ export class RawClient {
return (this._transport.request('chatlist_get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
}
/**
* get basic info about a chat,
* use chatlist_get_full_chat_by_id() instead if you need more information
*/
public getBasicChatInfo(accountId: T.U32, chatId: T.U32): Promise<T.BasicChat> {
return (this._transport.request('get_basic_chat_info', [accountId, chatId] as RPC.Params)) as Promise<T.BasicChat>;
}
public acceptChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('accept_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
@@ -222,169 +211,11 @@ export class RawClient {
return (this._transport.request('block_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Delete a chat.
*
* Messages are deleted from the device and the chat database entry is deleted.
* After that, the event #DC_EVENT_MSGS_CHANGED is posted.
*
* Things that are _not done_ implicitly:
*
* - Messages are **not deleted from the server**.
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
* and the user may create the chat again.
* - **Groups are not left** - this would
* be unexpected as (1) deleting a normal chat also does not prevent new mails
* from arriving, (2) leaving a group requires sending a message to
* all group members - especially for groups not used for a longer time, this is
* really unexpected when deletion results in contacting all members again,
* (3) only leaving groups is also a valid usecase.
*
* To leave a chat explicitly, use dc_remove_contact_from_chat() with
* chat_id=DC_CONTACT_ID_SELF)
*/
public deleteChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('delete_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Get encryption info for a chat.
* Get a multi-line encryption info, containing encryption preferences of all members.
* Can be used to find out why messages sent to group are not encrypted.
*
* returns Multi-line text
*/
public getChatEncryptionInfo(accountId: T.U32, chatId: T.U32): Promise<string> {
return (this._transport.request('get_chat_encryption_info', [accountId, chatId] as RPC.Params)) as Promise<string>;
}
/**
* Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
*
* The scanning device will pass the scanned content to `checkQr()` then;
* if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
* an out-of-band-verification can be joined using dc_join_securejoin()
*
* chat_id: If set to a group-chat-id,
* 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
* for details about both protocols.
*
* return format: `[code, svg]`
*/
public getChatSecurejoinQrCodeSvg(accountId: T.U32, chatId: (T.U32|null)): Promise<[string,string]> {
return (this._transport.request('get_chat_securejoin_qr_code_svg', [accountId, chatId] as RPC.Params)) as Promise<[string,string]>;
}
public leaveGroup(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('leave_group', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Remove a member from a group.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public removeContactFromChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('remove_contact_from_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
}
/**
* Add a member to a group.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* If the group has group protection enabled, only verified contacts can be added to the group.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public addContactToChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('add_contact_to_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
}
public addDeviceMessage(accountId: T.U32, label: string, text: string): Promise<T.U32> {
return (this._transport.request('add_device_message', [accountId, label, text] as RPC.Params)) as Promise<T.U32>;
}
/**
* Mark all messages in a chat as _noticed_.
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
* but are still waiting for being marked as "seen" using markseen_msgs()
* (IMAP/MDNs is not done for noticed messages).
*
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
* See also markseen_msgs().
*/
public marknoticedChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('marknoticed_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
public getFirstUnreadMessageOfChat(accountId: T.U32, chatId: T.U32): Promise<(T.U32|null)> {
return (this._transport.request('get_first_unread_message_of_chat', [accountId, chatId] as RPC.Params)) as Promise<(T.U32|null)>;
}
/**
* Set mute duration of a chat.
*
* The UI can then call is_chat_muted() when receiving a new message
* to decide whether it should trigger an notification.
*
* Muted chats should not sound or vibrate
* and should not show a visual notification in the system area.
* Moreover, muted chats should be excluded from global badge counter
* (get_fresh_msgs() skips muted chats therefore)
* and the in-app, per-chat badge counter should use a less obtrusive color.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*/
public setChatMuteDuration(accountId: T.U32, chatId: T.U32, duration: T.MuteDuration): Promise<null> {
return (this._transport.request('set_chat_mute_duration', [accountId, chatId, duration] as RPC.Params)) as Promise<null>;
}
/**
* Check whether the chat is currently muted (can be changed by set_chat_mute_duration()).
*
* This is available as a standalone function outside of fullchat, because it might be only needed for notification
*/
public isChatMuted(accountId: T.U32, chatId: T.U32): Promise<boolean> {
return (this._transport.request('is_chat_muted', [accountId, chatId] as RPC.Params)) as Promise<boolean>;
}
/**
* Mark messages as presented to the user.
* Typically, UIs call this function on scrolling through the message list,
* when the messages are presented at least for a little moment.
* The concrete action depends on the type of the chat and on the users settings
* (dc_msgs_presented() may be a better name therefore, but well. :)
*
* - For normal chats, the IMAP state is updated, MDN is sent
* (if set_config()-options `mdns_enabled` is set)
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
*
* - For contact requests, no IMAP or MDNs is done
* and the internal state is not changed therefore.
* See also marknoticed_chat().
*
* Moreover, timer is started for incoming ephemeral messages.
* This also happens for contact requests chats.
*
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
*/
public markseenMsgs(accountId: T.U32, msgIds: (T.U32)[]): Promise<null> {
return (this._transport.request('markseen_msgs', [accountId, msgIds] as RPC.Params)) as Promise<null>;
}
public messageListGetMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('message_list_get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
@@ -400,32 +231,6 @@ export class RawClient {
return (this._transport.request('message_get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
}
/**
* Fetch info desktop needs for creating a notification for a message
*/
public messageGetNotificationInfo(accountId: T.U32, messageId: T.U32): Promise<T.MessageNotificationInfo> {
return (this._transport.request('message_get_notification_info', [accountId, messageId] as RPC.Params)) as Promise<T.MessageNotificationInfo>;
}
/**
* Delete messages. The messages are deleted on the current device and
* on the IMAP server.
*/
public deleteMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<null> {
return (this._transport.request('delete_messages', [accountId, messageIds] as RPC.Params)) as Promise<null>;
}
/**
* Get an informational text for a single message. The text is multiline and may
* contain e.g. the raw text of the message.
*
* The max. text returned is typically longer (about 100000 characters) than the
* max. text returned by dc_msg_get_text() (about 30000 characters).
*/
public getMessageInfo(accountId: T.U32, messageId: T.U32): Promise<string> {
return (this._transport.request('get_message_info', [accountId, messageId] as RPC.Params)) as Promise<string>;
}
/**
* Get a single contact options by ID.
*/
@@ -482,15 +287,6 @@ export class RawClient {
return (this._transport.request('contacts_get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
}
/**
* Get encryption info for a contact.
* Get a multi-line encryption info, containing your fingerprint and the
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
*/
public getContactEncryptionInfo(accountId: T.U32, contactId: T.U32): Promise<string> {
return (this._transport.request('get_contact_encryption_info', [accountId, contactId] as RPC.Params)) as Promise<string>;
}
/**
* Returns all message IDs of the given types in a chat.
* Typically used to show a gallery.
@@ -498,69 +294,11 @@ export class RawClient {
* The list is already sorted and starts with the oldest message.
* Clients should not try to re-sort the list as this would be an expensive action
* and would result in inconsistencies between clients.
*
* Setting `chat_id` to `None` (`null` in typescript) means get messages with media
* from any chat of the currently used account.
*/
public chatGetMedia(accountId: T.U32, chatId: (T.U32|null), messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
public chatGetMedia(accountId: T.U32, chatId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
return (this._transport.request('chat_get_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Search next/previous message based on a given message and a list of types.
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
* one combined call for getting chat::get_next_media for both directions
* the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
*/
public chatGetNeighboringMedia(accountId: T.U32, msgId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<[(T.U32|null),(T.U32|null)]> {
return (this._transport.request('chat_get_neighboring_media', [accountId, msgId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<[(T.U32|null),(T.U32|null)]>;
}
/**
* Indicate that the network likely has come back.
* or just that the network conditions might have changed
*/
public maybeNetwork(): Promise<null> {
return (this._transport.request('maybe_network', [] as RPC.Params)) as Promise<null>;
}
/**
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
* One of:
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
*
* We don't use exact values but ranges here so that we can split up
* states into multiple states in the future.
*
* Meant as a rough overview that can be shown
* e.g. in the title of the main screen.
*
* If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*/
public getConnectivity(accountId: T.U32): Promise<T.U32> {
return (this._transport.request('get_connectivity', [accountId] as RPC.Params)) as Promise<T.U32>;
}
/**
* Get an overview of the current connectivity, and possibly more statistics.
* Meant to give the user more insight about the current status than
* the basic connectivity info returned by get_connectivity(); show this
* e.g., if the user taps on said basic connectivity info.
*
* If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*
* This comes as an HTML from the core so that we can easily improve it
* and the improvement instantly reaches all UIs.
*/
public getConnectivityHtml(accountId: T.U32): Promise<string> {
return (this._transport.request('get_connectivity_html', [accountId] as RPC.Params)) as Promise<string>;
}
public webxdcSendStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return (this._transport.request('webxdc_send_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
@@ -578,30 +316,6 @@ export class RawClient {
return (this._transport.request('message_get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
}
/**
* Forward messages to another chat.
*
* All types of messages can be forwarded,
* however, they will be flagged as such (dc_msg_is_forwarded() is set).
*
* Original sender, info-state and webxdc updates are not forwarded on purpose.
*/
public forwardMessages(accountId: T.U32, messageIds: (T.U32)[], chatId: T.U32): Promise<null> {
return (this._transport.request('forward_messages', [accountId, messageIds, chatId] as RPC.Params)) as Promise<null>;
}
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Get draft for a chat, if any.
*/
public getDraft(accountId: T.U32, chatId: T.U32): Promise<(T.Message|null)> {
return (this._transport.request('get_draft', [accountId, chatId] as RPC.Params)) as Promise<(T.Message|null)>;
}
/**
* Returns the messageid of the sent message
*/
@@ -610,14 +324,4 @@ export class RawClient {
}
public miscSendMsg(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), location: ([T.F64,T.F64]|null), quotedMessageId: (T.U32|null)): Promise<[T.U32,T.Message]> {
return (this._transport.request('misc_send_msg', [accountId, chatId, text, file, location, quotedMessageId] as RPC.Params)) as Promise<[T.U32,T.Message]>;
}
public miscSetDraft(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), quotedMessageId: (T.U32|null)): Promise<null> {
return (this._transport.request('misc_set_draft', [accountId, chatId, text, file, quotedMessageId] as RPC.Params)) as Promise<null>;
}
}

View File

@@ -1,3 +1,3 @@
// AUTO-GENERATED by typescript-type-def
export type EventTypeName=("Info"|"SmtpConnected"|"ImapConnected"|"SmtpMessageSent"|"ImapMessageDeleted"|"ImapMessageMoved"|"NewBlobFile"|"DeletedBlobFile"|"Warning"|"Error"|"ErrorSelfNotInGroup"|"MsgsChanged"|"IncomingMsg"|"MsgsNoticed"|"MsgDelivered"|"MsgFailed"|"MsgRead"|"ChatModified"|"ChatEphemeralTimerModified"|"ContactsChanged"|"LocationChanged"|"ConfigureProgress"|"ImexProgress"|"ImexFileWritten"|"SecurejoinInviterProgress"|"SecurejoinJoinerProgress"|"ConnectivityChanged"|"SelfavatarChanged"|"WebxdcStatusUpdate"|"WebXdInstanceDeleted");
export type EventTypeName=("Info"|"SmtpConnected"|"ImapConnected"|"SmtpMessageSent"|"ImapMessageDeleted"|"ImapMessageMoved"|"NewBlobFile"|"DeletedBlobFile"|"Warning"|"Error"|"ErrorSelfNotInGroup"|"MsgsChanged"|"IncomingMsg"|"MsgsNoticed"|"MsgDelivered"|"MsgFailed"|"MsgRead"|"ChatModified"|"ChatEphemeralTimerModified"|"ContactsChanged"|"LocationChanged"|"ConfigureProgress"|"ImexProgress"|"ImexFileWritten"|"SecurejoinInviterProgress"|"SecurejoinJoinerProgress"|"ConnectivityChanged"|"SelfavatarChanged"|"WebxdcStatusUpdate");

View File

@@ -3,54 +3,16 @@
export type U32=number;
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;}));
export type Usize=number;
export type ChatListEntry=[U32,U32];
export type I64=number;
export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name":string;"avatarPath":(string|null);"color":string;"lastUpdated":(I64|null);"summaryText1":string;"summaryText2":string;"summaryStatus":U32;"isProtected":boolean;"isGroup":boolean;"freshMessageCounter":Usize;"isSelfTalk":boolean;"isDeviceTalk":boolean;"isSendingLocation":boolean;"isSelfInGroup":boolean;"isArchived":boolean;"isPinned":boolean;"isMuted":boolean;"isContactRequest":boolean;
/**
* true when chat is a broadcastlist
*/
"isBroadcast":boolean;
/**
* contact id if this is a dm chat (for view profile entry in context menu)
*/
"dmChatContact":(U32|null);"wasSeenRecently":boolean;})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;
/**
* the contact's last seen timestamp
*/
"lastSeen":I64;"wasSeenRecently":boolean;};
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;"mailingListAddress":string;};
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
export type BasicChat=
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
export type MessageQuote=(({"kind":"JustText";}&{"text":string;})|({"kind":"WithMessage";}&{"text":string;"messageId":U32;"authorDisplayName":string;"authorDisplayColor":string;"overrideSenderName":(string|null);"image":(string|null);"isForwarded":boolean;}));
"dmChatContact":(U32|null);})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;};
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;};
export type Viewtype=("Unknown"|
/**
* Text message.
@@ -98,6 +60,7 @@ export type Viewtype=("Unknown"|
"Webxdc");
export type I32=number;
export type U64=number;
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quotedText":(string|null);"quotedMessageId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);};
export type WebxdcMessageInfo={
/**
* The name of the app.
@@ -130,21 +93,5 @@ export type WebxdcMessageInfo={
* defaults to an empty string.
* Implementations may offer an menu or a button to open this URL.
*/
"sourceCodeUrl":(string|null);
/**
* True if full internet access should be granted to the app.
*/
"internetAccess":boolean;};
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;};
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
/**
* also known as summary_text1
*/
"summaryPrefix":(string|null);
/**
* also known as summary_text2
*/
"summaryText":string;};
export type F64=number;
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,Message,U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,string,U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],null,U32,U32,U32,string,U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,null,U32,U32,(Message|null),U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
"sourceCodeUrl":(string|null);};
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,null,U32,U32,null,U32,string,string,U32,U32,U32,U32,(U32)[],U32,U32,Message,U32,(U32)[],Record<U32,Message>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,string,U32,U32];

View File

@@ -1,5 +1,29 @@
{
"name": "deltachat-jsonrpc-client",
"version": "0.1.0",
"main": "dist/deltachat.js",
"types": "dist/deltachat.d.ts",
"type": "module",
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
"license": "MPL-2.0",
"scripts": {
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
"generate-bindings": "cargo test",
"build": "run-s generate-bindings build:tsc build:bundle",
"build:tsc": "tsc",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"example": "run-s build example:build example:start",
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
"example:start": "http-server .",
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
"test": "run-s test:prepare test:run-coverage test:report-coverage",
"test:prepare": "cargo build --features webserver --bin deltachat-jsonrpc-server",
"test:run": "mocha dist/test",
"test:run-coverage": "COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include 'dist/*' -r text -r html -r json mocha dist/test",
"test:report-coverage": "node report_api_coverage.mjs",
"docs": "typedoc --out docs deltachat.ts"
},
"dependencies": {
"isomorphic-ws": "^4.0.1",
"tiny-emitter": "git+https://github.com/Simon-Laux/tiny-emitter.git",
@@ -23,29 +47,5 @@
"typedoc": "^0.23.2",
"typescript": "^4.5.5",
"ws": "^8.5.0"
},
"license": "MPL-2.0",
"main": "dist/deltachat.js",
"name": "@deltachat/jsonrpc-client",
"scripts": {
"build": "run-s generate-bindings build:tsc build:bundle",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"build:tsc": "tsc",
"docs": "typedoc --out docs deltachat.ts",
"example": "run-s build example:build example:start",
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
"example:start": "http-server .",
"generate-bindings": "cargo test",
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
"test": "run-s test:prepare test:run-coverage test:report-coverage",
"test:prepare": "cargo build --features webserver --bin deltachat-jsonrpc-server",
"test:report-coverage": "node report_api_coverage.mjs",
"test:run": "mocha dist/test",
"test:run-coverage": "COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include 'dist/*' -r text -r html -r json mocha dist/test"
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.96.0"
}
}
}

View File

@@ -1,4 +1,3 @@
#![allow(clippy::format_push_string)]
extern crate dirs;
use std::path::Path;
@@ -983,9 +982,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected.");
let images = chat::get_chat_media(
&context,
sel_chat.map(|c| c.id),
sel_chat.as_ref().unwrap().get_id(),
Viewtype::Image,
Viewtype::Gif,
Viewtype::Video,

View File

@@ -23,6 +23,7 @@ use deltachat::securejoin::*;
use deltachat::{EventType, Events};
use log::{error, info, warn};
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType;
use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
use rustyline::hint::{Hinter, HistoryHinter};
@@ -313,6 +314,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
.history_ignore_space(true)
.completion_type(CompletionType::List)
.edit_mode(EditMode::Emacs)
.output_stream(OutputStreamType::Stdout)
.build();
let mut selected_chat = ChatId::default();
@@ -323,7 +325,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
highlighter: MatchingBracketHighlighter::new(),
hinter: HistoryHinter {},
};
let mut rl = Editor::with_config(config)?;
let mut rl = Editor::with_config(config);
rl.set_helper(Some(h));
rl.bind_sequence(KeyEvent::alt('N'), Cmd::HistorySearchForward);
rl.bind_sequence(KeyEvent::alt('P'), Cmd::HistorySearchBackward);

View File

@@ -234,7 +234,7 @@ We have the following scripts for building, testing and coverage:
The following steps are needed to make a release:
1. Wait until `pack-module` github action is completed
2. Run `npm publish https://download.delta.chat/node/deltachat-node-1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
2. Run `npm publish https://download.delta.chat/node/deltachat-node-v1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
## License

View File

@@ -56,7 +56,6 @@ module.exports = {
DC_EVENT_SMTP_CONNECTED: 101,
DC_EVENT_SMTP_MESSAGE_SENT: 103,
DC_EVENT_WARNING: 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
DC_GCL_ADD_ALLDONE_HINT: 4,
DC_GCL_ADD_SELF: 2,
@@ -103,7 +102,6 @@ module.exports = {
DC_QR_FPR_MISMATCH: 220,
DC_QR_FPR_OK: 210,
DC_QR_FPR_WITHOUT_ADDR: 230,
DC_QR_LOGIN: 520,
DC_QR_REVIVE_VERIFYCONTACT: 510,
DC_QR_REVIVE_VERIFYGROUP: 512,
DC_QR_TEXT: 330,
@@ -130,10 +128,6 @@ module.exports = {
DC_STATE_UNDEFINED: 0,
DC_STR_AC_SETUP_MSG_BODY: 43,
DC_STR_AC_SETUP_MSG_SUBJECT: 42,
DC_STR_ADD_MEMBER_BY_OTHER: 129,
DC_STR_ADD_MEMBER_BY_YOU: 128,
DC_STR_AEAP_ADDR_CHANGED: 122,
DC_STR_AEAP_EXPLANATION_AND_LINK: 123,
DC_STR_ARCHIVEDCHATS: 40,
DC_STR_AUDIO: 11,
DC_STR_BAD_TIME_MSG_BODY: 85,
@@ -164,26 +158,6 @@ module.exports = {
DC_STR_EPHEMERAL_MINUTE: 77,
DC_STR_EPHEMERAL_MINUTES: 93,
DC_STR_EPHEMERAL_SECONDS: 76,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER: 147,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU: 146,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER: 145,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU: 144,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER: 143,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU: 142,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER: 149,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU: 148,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER: 155,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU: 154,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER: 139,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU: 138,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER: 153,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU: 152,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER: 151,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU: 150,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER: 141,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU: 140,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER: 157,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU: 156,
DC_STR_EPHEMERAL_WEEK: 80,
DC_STR_EPHEMERAL_WEEKS: 96,
DC_STR_ERROR: 112,
@@ -193,20 +167,10 @@ module.exports = {
DC_STR_FINGERPRINTS: 30,
DC_STR_FORWARDED: 97,
DC_STR_GIF: 23,
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER: 127,
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU: 126,
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER: 135,
DC_STR_GROUP_IMAGE_DELETED_BY_YOU: 134,
DC_STR_GROUP_LEFT_BY_OTHER: 133,
DC_STR_GROUP_LEFT_BY_YOU: 132,
DC_STR_GROUP_NAME_CHANGED_BY_OTHER: 125,
DC_STR_GROUP_NAME_CHANGED_BY_YOU: 124,
DC_STR_IMAGE: 9,
DC_STR_INCOMING_MESSAGES: 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY: 111,
DC_STR_LOCATION: 66,
DC_STR_LOCATION_ENABLED_BY_OTHER: 137,
DC_STR_LOCATION_ENABLED_BY_YOU: 136,
DC_STR_MESSAGES: 114,
DC_STR_MSGACTIONBYME: 63,
DC_STR_MSGACTIONBYUSER: 62,
@@ -226,16 +190,10 @@ module.exports = {
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
DC_STR_PART_OF_TOTAL_USED: 116,
DC_STR_PROTECTION_DISABLED: 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER: 161,
DC_STR_PROTECTION_DISABLED_BY_YOU: 160,
DC_STR_PROTECTION_ENABLED: 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER: 159,
DC_STR_PROTECTION_ENABLED_BY_YOU: 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
DC_STR_REMOVE_MEMBER_BY_YOU: 130,
DC_STR_REPLY_NOUN: 90,
DC_STR_SAVED_MESSAGES: 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC: 120,

View File

@@ -30,6 +30,5 @@ module.exports = {
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED'
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE'
}

View File

@@ -39,10 +39,6 @@ export class Chat {
return binding.dcn_chat_get_name(this.dc_chat)
}
getMailinglistAddr(): string {
return binding.dcn_chat_get_mailinglist_addr(this.dc_chat)
}
getProfileImage(): string {
return binding.dcn_chat_get_profile_image(this.dc_chat)
}
@@ -96,7 +92,6 @@ export class Chat {
color: this.color,
id: this.getId(),
name: this.getName(),
mailinglistAddr: this.getMailinglistAddr(),
profileImage: this.getProfileImage(),
type: this.getType(),
isSelfTalk: this.isSelfTalk(),

View File

@@ -56,7 +56,6 @@ export enum C {
DC_EVENT_SMTP_CONNECTED = 101,
DC_EVENT_SMTP_MESSAGE_SENT = 103,
DC_EVENT_WARNING = 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
@@ -103,7 +102,6 @@ export enum C {
DC_QR_FPR_MISMATCH = 220,
DC_QR_FPR_OK = 210,
DC_QR_FPR_WITHOUT_ADDR = 230,
DC_QR_LOGIN = 520,
DC_QR_REVIVE_VERIFYCONTACT = 510,
DC_QR_REVIVE_VERIFYGROUP = 512,
DC_QR_TEXT = 330,
@@ -130,10 +128,6 @@ export enum C {
DC_STATE_UNDEFINED = 0,
DC_STR_AC_SETUP_MSG_BODY = 43,
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
DC_STR_ADD_MEMBER_BY_OTHER = 129,
DC_STR_ADD_MEMBER_BY_YOU = 128,
DC_STR_AEAP_ADDR_CHANGED = 122,
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BAD_TIME_MSG_BODY = 85,
@@ -164,26 +158,6 @@ export enum C {
DC_STR_EPHEMERAL_MINUTE = 77,
DC_STR_EPHEMERAL_MINUTES = 93,
DC_STR_EPHEMERAL_SECONDS = 76,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
DC_STR_EPHEMERAL_WEEK = 80,
DC_STR_EPHEMERAL_WEEKS = 96,
DC_STR_ERROR = 112,
@@ -193,20 +167,10 @@ export enum C {
DC_STR_FINGERPRINTS = 30,
DC_STR_FORWARDED = 97,
DC_STR_GIF = 23,
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
DC_STR_GROUP_LEFT_BY_OTHER = 133,
DC_STR_GROUP_LEFT_BY_YOU = 132,
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
DC_STR_IMAGE = 9,
DC_STR_INCOMING_MESSAGES = 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
DC_STR_LOCATION = 66,
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
DC_STR_MESSAGES = 114,
DC_STR_MSGACTIONBYME = 63,
DC_STR_MSGACTIONBYUSER = 62,
@@ -226,16 +190,10 @@ export enum C {
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
@@ -299,5 +257,4 @@ export const EventId2EventName: { [key: number]: string } = {
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
}

View File

@@ -72,10 +72,6 @@ export class Contact {
return binding.dcn_contact_get_last_seen(this.dc_contact)
}
wasSeenRecently() {
return Boolean(binding.dcn_contact_was_seen_recently(this.dc_contact))
}
getName(): string {
return binding.dcn_contact_get_name(this.dc_contact)
}

View File

@@ -115,7 +115,7 @@ export class AccountManager extends EventEmitter {
debug('Started event handler')
}
startJsonRpcHandler(callback: ((response: string) => void) | null) {
startJsonRpcHandler(callback: ((response: string) => void) | null, apiVersion: string = "v0") {
if (this.dcn_accounts === null) {
throw new Error('dcn_account is null')
}
@@ -126,7 +126,7 @@ export class AccountManager extends EventEmitter {
throw new Error('jsonrpc was started already')
}
binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, callback.bind(this))
binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, apiVersion, callback.bind(this))
debug('Started JSON-RPC handler')
this.jsonRpcStarted = true
}

View File

@@ -12,7 +12,6 @@ export interface ChatJSON {
color: string
id: number
name: string
mailinglistAddr: string
profileImage: string
type: number
isSelfTalk: boolean

View File

@@ -12,12 +12,11 @@ const GITHUB_API_URL =
const file_url = process.env['URL']
const GITHUB_TOKEN = process.env['GITHUB_TOKEN']
const context = process.env['MSG_CONTEXT']
const STATUS_DATA = {
state: 'success',
description: '⏩ Click on "Details" to download →',
context: context || 'Download the node-bindings.tar.gz',
context: 'Download the node-bindings.tar.gz',
target_url: base_url + file_url,
}

28
node/segfault.js Normal file
View File

@@ -0,0 +1,28 @@
const { default: dc } = require('./dist')
const ac = new dc('test1233490')
console.log("[1]");
ac.startJsonRpcHandler(console.log)
console.log("[2]");
console.log(
ac.jsonRpcRequest(
JSON.stringify({
jsonrpc: '2.0',
method: 'get_all_account_ids',
params: [],
id: 2,
})
)
)
console.log("[3]");
setTimeout(() => {
console.log("[4]");
ac.close() // This segfaults -> TODO Findout why?
console.log('still living')
}, 1000)

50
node/segfault2.js Normal file
View File

@@ -0,0 +1,50 @@
const { default: dc } = require('./dist')
const ac = new dc('test1233490')
console.log('[1]')
ac.startJsonRpcHandler(console.log)
console.log('[2]')
console.log(
ac.jsonRpcRequest(
JSON.stringify({
jsonrpc: '2.0',
method: 'batch_set_config',
id: 3,
params: [
69,
{
addr: '',
mail_user: '',
mail_pw: '',
mail_server: '',
mail_port: '',
mail_security: '',
imap_certificate_checks: '',
send_user: '',
send_pw: '',
send_server: '',
send_port: '',
send_security: '',
smtp_certificate_checks: '',
socks5_enabled: '0',
socks5_host: '',
socks5_port: '',
socks5_user: '',
socks5_password: '',
},
],
})
)
)
console.log('[3]')
setTimeout(() => {
console.log('[4]')
ac.close() // This segfaults -> TODO Findout why?
console.log('still living')
}, 1000)

View File

@@ -1628,18 +1628,6 @@ NAPI_METHOD(dcn_chat_get_name) {
NAPI_RETURN_AND_UNREF_STRING(name);
}
NAPI_METHOD(dcn_chat_get_mailinglist_addr) {
NAPI_ARGV(1);
NAPI_DC_CHAT();
//TRACE("calling..");
char* addr = dc_chat_get_mailinglist_addr(dc_chat);
//TRACE("result %s", name);
NAPI_RETURN_AND_UNREF_STRING(addr);
}
NAPI_METHOD(dcn_chat_get_profile_image) {
NAPI_ARGV(1);
NAPI_DC_CHAT();
@@ -1930,13 +1918,6 @@ NAPI_METHOD(dcn_contact_get_last_seen) {
NAPI_RETURN_INT64(timestamp);
}
NAPI_METHOD(dcn_contact_was_seen_recently) {
NAPI_ARGV(1);
NAPI_DC_CONTACT();
int seen_recently = dc_contact_was_seen_recently(dc_contact);
NAPI_RETURN_UINT32(seen_recently);
}
NAPI_METHOD(dcn_contact_is_blocked) {
NAPI_ARGV(1);
NAPI_DC_CONTACT();
@@ -3112,14 +3093,14 @@ static void accounts_event_handler_thread_func(void* arg)
TRACE("event_handler_thread_func starting");
dc_event_emitter_t * dc_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts);
dc_accounts_event_emitter_t * dc_accounts_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts);
dc_event_t* event;
while (true) {
if (dc_event_emitter == NULL) {
if (dc_accounts_event_emitter == NULL) {
TRACE("event emitter is null, bailing");
break;
}
event = dc_get_next_event(dc_event_emitter);
event = dc_accounts_get_next_event(dc_accounts_event_emitter);
if (event == NULL) {
TRACE("no more events");
break;
@@ -3145,7 +3126,7 @@ static void accounts_event_handler_thread_func(void* arg)
}
}
dc_event_emitter_unref(dc_event_emitter);
dc_accounts_event_emitter_unref(dc_accounts_event_emitter);
TRACE("event_handler_thread_func ended");
@@ -3332,9 +3313,10 @@ static void call_accounts_js_jsonrpc_handler(napi_env env, napi_value js_callbac
}
NAPI_METHOD(dcn_accounts_start_jsonrpc) {
NAPI_ARGV(2);
NAPI_ARGV(3);
NAPI_DCN_ACCOUNTS();
napi_value callback = argv[1];
NAPI_ARGV_UTF8_MALLOC(api_version, 1);
napi_value callback = argv[2];
TRACE("calling..");
napi_value async_resource_name;
@@ -3357,7 +3339,7 @@ NAPI_METHOD(dcn_accounts_start_jsonrpc) {
TRACE("done");
dcn_accounts->gc = 0;
dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts);
dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts, api_version);
TRACE("creating uv thread..");
uv_thread_create(&dcn_accounts->jsonrpc_thread, accounts_jsonrpc_thread_func, dcn_accounts);
@@ -3514,7 +3496,6 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_chat_get_visibility);
NAPI_EXPORT_FUNCTION(dcn_chat_get_id);
NAPI_EXPORT_FUNCTION(dcn_chat_get_name);
NAPI_EXPORT_FUNCTION(dcn_chat_get_mailinglist_addr);
NAPI_EXPORT_FUNCTION(dcn_chat_get_profile_image);
NAPI_EXPORT_FUNCTION(dcn_chat_get_type);
NAPI_EXPORT_FUNCTION(dcn_chat_is_self_talk);

View File

@@ -1,5 +1,6 @@
{
"dependencies": {
"@deltachat/jsonrpc-client": "file:deltachat-jsonrpc/typescript",
"debug": "^4.1.1",
"napi-macros": "^2.0.0",
"node-gyp-build": "^4.1.0"
@@ -60,5 +61,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail"
},
"types": "node/dist/index.d.ts",
"version": "1.96.0"
"version": "1.87.0"
}

View File

@@ -1,77 +1,84 @@
=========================
DeltaChat Python bindings
deltachat python bindings
=========================
This package provides `Python bindings`_ to the `deltachat-core library`_
which implements IMAP/SMTP/MIME/OpenPGP e-mail standards and offers
This package provides bindings to the deltachat-core_ Rust -library
which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
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 try to install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps.
Otherwise you need to `compile the Delta Chat bindings yourself`__.
__ sourceinstall_
Otherwise you need to `compile the Delta Chat bindings yourself <#sourceinstall>`_.
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation.html>`_,
then create a fresh Python virtual environment and activate it in your shell::
virtualenv env # or: python -m venv
source env/bin/activate
virtualenv venv # or: python -m venv
source venv/bin/activate
Afterwards, invoking ``python`` or ``pip install`` only
modifies files in your ``env`` directory and leaves
modifies files in your ``venv`` 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::
For Linux, we automatically build wheels for all github PR branches
and push them to a python package index. To install the latest
github ``master`` branch::
pip install deltachat
pip install --pre -i https://m.devpi.net/dc/master deltachat
To verify it worked::
python -c "import deltachat"
.. note::
If you can help to automate the building of wheels for Mac or Windows,
that'd be much appreciated! please then get
`in contact with us <https://delta.chat/en/contribute>`_.
Running tests
=============
Recommended way to run tests is using `tox <https://tox.wiki>`_.
After successful binding installation you can install tox
and run the tests::
After successful binding installation you can install a few more
Python packages before running the tests::
pip install tox
tox -e py3
python -m pip install pytest pytest-xdist pytest-timeout pytest-rerunfailures requests
pytest -v tests
This will run all "offline" tests and skip all functional
end-to-end tests that require accounts on real e-mail servers.
.. _livetests:
Running "live" tests with temporary accounts
--------------------------------------------
running "live" tests with temporary accounts
---------------------------------------------
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLs created and managed by `mailadm <https://mailadm.readthedocs.io/>`_.
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLS created and managed by [mailadm](https://mailadm.readthedocs.io/en/latest/).
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this::
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this:
export DCC_NEW_TMP_EMAIL=<URL you got from us>
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server. These accounts exists only for one hour and then are removed completely.
One hour is enough to invoke pytest and run all offline and online tests::
One hour is enough to invoke pytest and run all offline and online tests:
tox -e py3
pytest
# or if you have installed pytest-xdist for parallel test execution
pytest -n6
Each test run creates new accounts.
.. _sourceinstall:
Installing bindings from source
===============================
Installing bindings from source (Updated: July 2020)
=========================================================
Install Rust and Cargo first.
The easiest is probably to use `rustup <https://rustup.rs/>`_.
@@ -90,42 +97,74 @@ E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
Ensure you are in the deltachat-core-rust/python directory, create the
virtual environment with dependencies using tox
and activate it in your shell::
virtual environment and activate it in your shell::
cd python
tox --devenv env
source env/bin/activate
python3 -m venv venv # or: virtualenv venv
source venv/bin/activate
You should now be able to build the python bindings using the supplied script::
python3 install_python_bindings.py
python install_python_bindings.py
The core compilation and bindings building might take a while,
depending on the speed of your machine.
The bindings will be installed in release mode but with debug symbols.
The release mode is currently necessary because some tests generate RSA keys
which is prohibitively slow in non-release mode.
Code examples
=============
You may look at `examples <https://py.delta.chat/examples.html>`_.
.. _`deltachat-core-rust github repository`: https://github.com/deltachat/deltachat-core-rust
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
Building manylinux based wheels
===============================
====================================
Building portable manylinux wheels which come with libdeltachat.so
can be done with Docker_ or Podman_.
can be done with docker-tooling.
.. _Docker: https://www.docker.com/
.. _Podman: https://podman.io/
using docker pull / premade images
------------------------------------
If you want to build your own wheels, build container image first::
We publish a build environment under the ``deltachat/coredeps`` tag so
that you can pull it from the ``hub.docker.com`` site's "deltachat"
organization::
$ 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
$ docker pull deltachat/coredeps
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e DCC_NEW_TMP_EMAIL \
--rm -it -v $(pwd):/mnt -w /mnt \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps scripts/run_all.sh
Optionally build your own docker image
--------------------------------------
If you want to build your own custom docker image you can do this::
$ cd deltachat-core # cd to deltachat-core checkout directory
$ docker build -t deltachat/coredeps scripts/docker_coredeps
This will use the ``scripts/docker_coredeps/Dockerfile`` to build
up docker image called ``deltachat/coredeps``. You can afterwards
find it with::
$ docker images
Troubleshooting
---------------
On more recent systems running the docker image may crash. You can
fix this by adding ``vsyscall=emulate`` to the Linux kernel boot
arguments commandline. E.g. on Debian you'd add this to
``GRUB_CMDLINE_LINUX_DEFAULT`` in ``/etc/default/grub``.

7
python/fail_test.py Normal file
View File

@@ -0,0 +1,7 @@
from __future__ import print_function
from deltachat import capi
from deltachat.capi import ffi, lib
if __name__ == "__main__":
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
lib.dc_stop_io(ctx)

View File

@@ -16,7 +16,7 @@ if __name__ == "__main__":
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.environ["DCC_RS_DEV"] = dn
cmd = ["cargo", "build", "-p", "deltachat_ffi", "--features", "jsonrpc"]
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
if target == "release":
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"

View File

@@ -1,41 +1,7 @@
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2", "cffi>=1.0.0", "pkgconfig"]
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0", "pkgconfig"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.7"
authors = [
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Programming Language :: Python :: 3",
"Topic :: Communications :: Email",
"Topic :: Software Development :: Libraries",
]
dependencies = [
"cffi>=1.0.0",
"imap-tools",
"pluggy",
"requests",
]
dynamic = [
"version"
]
[project.urls]
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
"Documentation" = "https://py.delta.chat/"
[project.entry-points.pytest11]
"deltachat.testplugin" = "deltachat.testplugin"
[tool.setuptools_scm]
root = ".."
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'

4
python/setup.cfg Normal file
View File

@@ -0,0 +1,4 @@
[devpi:upload]
formats = sdist.tgz
no-vcs = 1

View File

@@ -1,4 +1,40 @@
from setuptools import setup
import os
import re
import setuptools
def main():
with open("README.rst") as f:
long_description = f.read()
setuptools.setup(
name="deltachat",
description="Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat",
long_description=long_description,
author="holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors",
install_requires=["cffi>=1.0.0", "pluggy", "imap-tools", "requests"],
setup_requires=[
"setuptools_scm", # required for compatibility with `python3 setup.py sdist`
"pkgconfig",
],
packages=setuptools.find_packages("src"),
package_dir={"": "src"},
cffi_modules=["src/deltachat/_build.py:ffibuilder"],
entry_points={
"pytest11": [
"deltachat.testplugin = deltachat.testplugin",
],
},
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Programming Language :: Python :: 3",
"Topic :: Communications :: Email",
"Topic :: Software Development :: Libraries",
],
)
if __name__ == "__main__":
setup(cffi_modules=["src/deltachat/_build.py:ffibuilder"])
main()

View File

@@ -2,7 +2,7 @@ import sys
from pkg_resources import DistributionNotFound, get_distribution
from . import capi, events, hookspec # noqa
from . import capi, const, events, hookspec # noqa
from .account import Account, get_core_info # noqa
from .capi import ffi # noqa
from .chat import Chat # noqa
@@ -17,6 +17,14 @@ except DistributionNotFound:
__version__ = "0.0.0.dev0-unknown"
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
if not _DC_EVENTNAME_MAP:
for name in dir(const):
if name.startswith("DC_EVENT_"):
_DC_EVENTNAME_MAP[getattr(const, name)] = name
return _DC_EVENTNAME_MAP[integer]
def register_global_plugin(plugin):
"""Register a global plugin which implements one or more
of the :class:`deltachat.hookspec.Global` hooks.
@@ -52,7 +60,29 @@ def run_cmdline(argv=None, account_plugins=None):
ac = Account(args.db)
ac.run_account(addr=args.email, password=args.password, account_plugins=account_plugins, show_ffi=args.show_ffi)
if args.show_ffi:
ac.set_config("displayname", "bot")
log = events.FFIEventLogger(ac)
ac.add_account_plugin(log)
for plugin in account_plugins or []:
print("adding plugin", plugin)
ac.add_account_plugin(plugin)
if not ac.is_configured():
assert (
args.email and args.password
), "you must specify --email and --password once to configure this database/account"
ac.set_config("addr", args.email)
ac.set_config("mail_pw", args.password)
ac.set_config("mvbox_move", "0")
ac.set_config("sentbox_watch", "0")
ac.set_config("bot", "1")
configtracker = ac.configure()
configtracker.wait_finish()
# start IO threads and configure if neccessary
ac.start_io()
print("{}: waiting for message".format(ac.get_config("addr")))

View File

@@ -20,7 +20,7 @@ from .cutil import (
from_optional_dc_charpointer,
iter_array,
)
from .events import EventThread, FFIEventLogger
from .events import EventThread
from .message import Message
from .tracker import ConfigureTracker, ImexTracker
@@ -169,6 +169,8 @@ class Account(object):
"""
self._check_config_key(name)
namebytes = name.encode("utf8")
if namebytes == b"addr" and self.is_configured():
raise ValueError("can not change 'addr' after account is configured.")
if isinstance(value, (int, bool)):
value = str(int(value))
if value is not None:
@@ -596,36 +598,6 @@ class Account(object):
# meta API for start/stop and event based processing
#
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
"""get the account running, configure it if necessary. add plugins if provided.
:param addr: the email address of the account
:param password: the password of the account
:param account_plugins: a list of plugins to add
:param show_ffi: show low level ffi events
"""
if show_ffi:
self.set_config("displayname", "bot")
log = FFIEventLogger(self)
self.add_account_plugin(log)
for plugin in account_plugins or []:
print("adding plugin", plugin)
self.add_account_plugin(plugin)
if not self.is_configured():
assert addr and password, "you must specify email and password once to configure this database/account"
self.set_config("addr", addr)
self.set_config("mail_pw", password)
self.set_config("mvbox_move", "0")
self.set_config("sentbox_watch", "0")
self.set_config("bot", "1")
configtracker = self.configure()
configtracker.wait_finish()
# start IO threads and configure if neccessary
self.start_io()
def add_account_plugin(self, plugin, name=None):
"""add an account plugin which implements one or more of
the :class:`deltachat.hookspec.PerAccount` hooks.
@@ -708,9 +680,8 @@ class Account(object):
"""Start configuration process and return a Configtracker instance
on which you can block with wait_finish() to get a True/False success
value for the configuration process.
:param reconfigure: deprecated, doesn't need to be checked anymore.
"""
assert self.is_configured() == reconfigure
if not self.get_config("addr") or not self.get_config("mail_pw"):
raise MissingCredentials("addr or mail_pwd not set in config")
configtracker = ConfigureTracker(self)

View File

@@ -8,21 +8,14 @@ import traceback
from contextlib import contextmanager
from queue import Empty, Queue
from . import const
import deltachat
from .capi import ffi, lib
from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
from .message import map_system_message
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
if not _DC_EVENTNAME_MAP:
for name in dir(const):
if name.startswith("DC_EVENT_"):
_DC_EVENTNAME_MAP[getattr(const, name)] = name
return _DC_EVENTNAME_MAP[integer]
class FFIEvent:
def __init__(self, name: str, data1, data2):
self.name = name
@@ -246,7 +239,7 @@ class EventThread(threading.Thread):
data1 = lib.dc_event_get_data1_int(event)
# the following code relates to the deltachat/_build.py's helper
# function which provides us signature info of an event call
evt_name = get_dc_event_name(evt)
evt_name = deltachat.get_dc_event_name(evt)
if lib.dc_event_has_string_data(evt):
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
else:

View File

@@ -507,8 +507,6 @@ def parse_system_add_remove(text):
returns a (action, affected, actor) triple"""
# You removed member a@b.
# You added member a@b.
# Member Me (x@y) removed by a@b.
# Member x@y added by a@b
# Member With space (tmp1@x.org) removed by tmp2@x.org.
@@ -520,10 +518,6 @@ def parse_system_add_remove(text):
if m:
affected, action, actor = m.groups()
return action, extract_addr(affected), extract_addr(actor)
m = re.match(r"you (removed|added) member (.+)", text)
if m:
action, affected = m.groups()
return action, extract_addr(affected), "me"
if text.startswith("group left by "):
addr = extract_addr(text[13:])
if addr:

View File

@@ -497,8 +497,6 @@ class ACFactory:
configdict = dict(
addr=cloned_from.get_config("addr"),
mail_pw=cloned_from.get_config("mail_pw"),
imap_certificate_checks=cloned_from.get_config("imap_certificate_checks"),
smtp_certificate_checks=cloned_from.get_config("smtp_certificate_checks"),
)
configdict.update(kwargs)
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None

View File

@@ -0,0 +1,13 @@
import os
import subprocess
import sys
if __name__ == "__main__":
assert len(sys.argv) == 2
wheelhousedir = sys.argv[1]
# pip wheel will build in an isolated tmp dir that does not have git
# history so setuptools_scm can not automatically determine a
# version there. So pass in the version through an env var.
version = subprocess.check_output(["python", "setup.py", "--version"]).strip().split(b"\n")[-1]
os.environ["SETUPTOOLS_SCM_PRETEND_VERSION"] = version.decode("ascii")
subprocess.check_call(("pip wheel . -w %s" % wheelhousedir).split())

View File

@@ -1581,9 +1581,8 @@ def test_set_get_group_image(acfactory, data, lp):
lp.sec("ac2: wait for receiving message from ac1")
msg1 = ac2._evtracker.wait_next_incoming_message()
assert msg1.is_system_message() # Member added
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "hi"
assert msg1.text == "hi" or msg2.text == "hi"
assert msg1.chat.id == msg2.chat.id
lp.sec("ac2: see if chat now has got the profile image")
@@ -1597,8 +1596,6 @@ def test_set_get_group_image(acfactory, data, lp):
lp.sec("ac2: delete profile image from chat")
msg1.chat.remove_profile_image()
msg_back = ac1._evtracker.wait_next_incoming_message()
assert msg_back.text == "Group image deleted by {}.".format(ac2.get_config("addr"))
assert msg_back.is_system_message()
assert msg_back.chat == chat
assert chat.get_profile_image() is None
@@ -1857,7 +1854,7 @@ def test_configure_error_msgs_invalid_server(acfactory):
# Can't connect so it probably should say something about "internet"
# again, should not repeat itself
# If this fails then probably `e.msg.to_lowercase().contains("could not resolve")`
# in configure.rs returned false because the error message was changed
# in configure/mod.rs returned false because the error message was changed
# (i.e. did not contain "could not resolve" anymore)
assert (ev.data2.count("internet") + ev.data2.count("network")) == 1
# Should mention that it can't connect:
@@ -2055,47 +2052,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()]
class TestOnlineConfigureFails:
def test_invalid_password(self, acfactory):
configdict = acfactory.get_next_liveconfig()

View File

@@ -80,7 +80,7 @@ class TestOfflineAccountBasic:
d = ac1.get_info()
assert d["arch"]
assert d["number_of_chats"] == "0"
assert d["bcc_self"] == "1"
assert d["bcc_self"] == "0"
def test_is_not_configured(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
@@ -118,7 +118,7 @@ class TestOfflineAccountBasic:
def test_has_bccself(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
assert ac1.get_config("bcc_self") == "1"
assert ac1.get_config("bcc_self") == "0"
def test_selfcontact_if_unconfigured(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
@@ -301,7 +301,7 @@ class TestOfflineChat:
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1):
ac1.set_stock_translation(const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "abc %1$s xyz %2$s")
ac1._evtracker.consume_events()
with pytest.raises(ValueError):
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
@@ -317,7 +317,7 @@ class TestOfflineChat:
chat.send_text("Now we have a group for homework")
assert chat.is_promoted()
chat.set_name("Homework")
assert chat.get_messages()[-1].text == "abc homework xyz Homework"
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
@pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified):
@@ -497,6 +497,12 @@ class TestOfflineChat:
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
def test_set_config_after_configure_is_forbidden(self, ac1):
assert ac1.get_config("mail_pw")
assert ac1.is_configured()
with pytest.raises(ValueError):
ac1.set_config("addr", "123@example.org")
def test_import_export_on_unencrypted_acct(self, acfactory, tmpdir):
backupdir = tmpdir.mkdir("backup")
ac1 = acfactory.get_pseudo_configured_account()

View File

@@ -9,8 +9,9 @@ envlist =
[testenv]
commands =
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
pip wheel . -w {toxworkdir}/wheelhouse --no-deps
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
DCC_RS_DEV
DCC_RS_TARGET
DCC_NEW_TMP_EMAIL

View File

@@ -14,15 +14,18 @@ and an own build machine.
- `remote_tests_rust.sh` rsyncs to the build machine and runs
`run-rust-test.sh` remotely on the build machine.
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
- `doxygen/Dockerfile` specifies an image that contains
the doxygen tool which is used by `run-doxygen.sh`
to generate C-docs which are then uploaded
via `ci_upload.sh` to `https://c.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
(and the master branch is linked to https://c.delta.chat proper).
- `run_all.sh` builds Python wheels
## Triggering runs on the build machine locally (fast!)
There is experimental support for triggering a remote Python or Rust test run
from your local checkout/branch. You will need to be authorized to login to
the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type:
the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type::
scripts/manual_remote_tests.sh rust
scripts/manual_remote_tests.sh python
@@ -30,18 +33,19 @@ the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type
This will **rsync** your current checkout to the remote build machine
(no need to commit before) and then run either rust or python tests.
# coredeps Dockerfile
# Outdated files (for later re-use)
`coredeps/Dockerfile` specifies an image that contains all
of Delta Chat's core dependencies. It is used to
build python wheels (binary packages for Python).
of Delta Chat's core dependencies. It used to run
python tests and build wheels (binary packages for Python)
You can build the docker images yourself locally
to avoid the relatively large download:
to avoid the relatively large download::
cd scripts # where all CI things are
docker build -t deltachat/coredeps coredeps
docker build -t deltachat/coredeps docker-coredeps
docker build -t deltachat/doxygen docker-doxygen
Additionally, you can install qemu and build arm64 docker image on x86\_64 machine:
Additionally, you can install qemu and build arm64 docker image:
apt-get install qemu binfmt-support qemu-user-static
docker build -t deltachat/coredeps-arm64 --build-arg BASEIMAGE=quay.io/pypa/manylinux2014_aarch64 coredeps
docker build -t deltachat/coredeps-arm64 docker-coredeps-arm64

View File

@@ -29,19 +29,18 @@ jobs:
- name: c-docs
image_resource:
source:
repository: alpine
repository: hrektts/doxygen
type: registry-image
platform: linux
run:
path: sh
path: bash
args:
- -ec
- -exc
- |
apk add --no-cache doxygen git
cd deltachat-core-rust
scripts/run-doxygen.sh
bash scripts/run-doxygen.sh
cd ..
cp -av deltachat-core-rust/deltachat-ffi/html deltachat-core-rust/deltachat-ffi/xml c-docs/
cp -av deltachat-core-rust/deltachat-ffi/{html,xml} c-docs/
- task: upload-c-docs
config:
@@ -303,73 +302,3 @@ jobs:
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*musllinux_1_1_x86_64*
- name: python-musl-aarch64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
repository: concourse/oci-build-task
type: registry-image
outputs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/coredeps
UNPACK_ROOTFS: "true"
BUILD_ARG_BASEIMAGE: quay.io/pypa/musllinux_1_1_aarch64
platform: linux
caches:
- path: cache
run:
path: build
# Use built image to build python wheels
- task: build-wheels
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
platform: linux
run:
path: bash
args:
- -exc
- |
scripts/run_all.sh
# Upload musl aarch64 wheels
- task: upload-wheels
config:
inputs:
- name: py-wheels
image_resource:
type: registry-image
source:
repository: debian
platform: linux
run:
path: sh
args:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools
pip3 install devpi
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*musllinux_1_1_aarch64*

View File

@@ -17,7 +17,7 @@ export RUSTC_BOOTSTRAP=1
# [2] https://github.com/mozilla/grcov/issues/595
export CARGO_INCREMENTAL=0
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebuginfo=2"
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
export RUSTDOCFLAGS="-Cpanic=abort"
cargo clean
cargo build

View File

@@ -0,0 +1,5 @@
FROM debian:stable
# this is tagged as deltachat/doxygen
RUN apt-get update && apt-get install -y doxygen

View File

@@ -1,5 +1,6 @@
#!/bin/sh
set -e
#!/usr/bin/env bash
set -ex
cd deltachat-ffi
PROJECT_NUMBER=$(git log -1 --format="%h (%cd)") doxygen

View File

@@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
#
# Build the Delta Chat Core Rust library, Python wheels and docs
@@ -8,16 +8,21 @@ set -e -x
# compile core lib
cargo build --release -p deltachat_ffi --features jsonrpc
export PATH=/root/.cargo/bin:$PATH
cargo build --release -p deltachat_ffi
# cargo test --all --all-features
# Statically link against libdeltachat.a.
export DCC_RS_DEV="$PWD"
export DCC_RS_DEV=$(pwd)
export DCC_RS_TARGET=release
# Configure access to a base python and to several python interpreters
# needed by tox below.
export PATH=$PATH:/opt/python/cp37-cp37m/bin
export PYTHONDONTWRITEBYTECODE=1
cd python
TOXWORKDIR=.docker-tox
pushd python
# prepare a clean tox run
rm -rf tests/__pycache__
rm -rf src/deltachat/__pycache__
@@ -28,13 +33,11 @@ mkdir -p $TOXWORKDIR
# Note that the independent remote_tests_python step does all kinds of
# live-testing already.
unset DCC_NEW_TMP_EMAIL
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,pypy37,pypy38,pypy39,auditwheels --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,pypy37,pypy38,pypy39,auditwheels
popd
echo -----------------------
echo generating python docs
echo -----------------------
tox --workdir "$TOXWORKDIR" -e doc
(cd python && tox --workdir "$TOXWORKDIR" -e doc)

19
scripts/set_core_version.py Executable file → Normal file
View File

@@ -42,15 +42,15 @@ def replace_toml_version(relpath, newversion):
def read_json_version(relpath):
p = pathlib.Path(relpath)
p = pathlib.Path("package.json")
assert p.exists()
with open(p, "r") as f:
json_data = json.loads(f.read())
return json_data["version"]
def update_package_json(relpath, newversion):
p = pathlib.Path(relpath)
def update_package_json(newversion):
p = pathlib.Path("package.json")
assert p.exists()
with open(p, "r") as f:
json_data = json.loads(f.read())
@@ -64,15 +64,13 @@ def main():
parser.add_argument("newversion")
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml", "deltachat-jsonrpc/Cargo.toml"]
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
try:
opts = parser.parse_args()
except SystemExit:
print()
for x in toml_list:
print("{}: {}".format(x, read_toml_version(x)))
for x in json_list:
print("{}: {}".format(x, read_json_version(x)))
print("package.json:", str(read_json_version("package.json")))
print()
raise SystemExit("need argument: new version, example: 1.25.0")
@@ -93,12 +91,9 @@ def main():
else:
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
for toml_filename in toml_list:
replace_toml_version(toml_filename, newversion)
for json_filename in json_list:
update_package_json(json_filename, newversion)
replace_toml_version("Cargo.toml", newversion)
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
update_package_json(newversion)
print("running cargo check")
subprocess.call(["cargo", "check"])

View File

@@ -33,7 +33,7 @@ impl Accounts {
}
/// Creates a new default structure.
pub async fn create(dir: &Path) -> Result<()> {
pub async fn create(dir: &PathBuf) -> Result<()> {
fs::create_dir_all(dir)
.await
.context("failed to create folder")?;
@@ -69,19 +69,19 @@ impl Accounts {
}
/// Get an account by its `id`:
pub fn get_account(&self, id: u32) -> Option<Context> {
pub async fn get_account(&self, id: u32) -> Option<Context> {
self.accounts.get(&id).cloned()
}
/// Get the currently selected account.
pub fn get_selected_account(&self) -> Option<Context> {
let id = self.config.get_selected_account();
pub async fn get_selected_account(&self) -> Option<Context> {
let id = self.config.get_selected_account().await;
self.accounts.get(&id).cloned()
}
/// Returns the currently selected account's id or None if no account is selected.
pub fn get_selected_account_id(&self) -> Option<u32> {
match self.config.get_selected_account() {
pub async fn get_selected_account_id(&self) -> Option<u32> {
match self.config.get_selected_account().await {
0 => None,
id => Some(id),
}
@@ -135,7 +135,7 @@ impl Accounts {
ctx.stop_io().await;
drop(ctx);
if let Some(cfg) = self.config.get_account(id) {
if let Some(cfg) = self.config.get_account(id).await {
// Spend up to 1 minute trying to remove the files.
// Files may remain locked up to 30 seconds due to r2d2 bug:
// https://github.com/sfackler/r2d2/issues/99
@@ -171,7 +171,7 @@ impl Accounts {
ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
let old_id = self.config.get_selected_account();
let old_id = self.config.get_selected_account().await;
// create new account
let account_config = self
@@ -225,7 +225,7 @@ impl Accounts {
}
/// Get a list of all account ids.
pub fn get_all(&self) -> Vec<u32> {
pub async fn get_all(&self) -> Vec<u32> {
self.accounts.keys().copied().collect()
}
@@ -281,7 +281,7 @@ impl Accounts {
}
/// Returns event emitter.
pub fn get_event_emitter(&self) -> EventEmitter {
pub async fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter()
}
}
@@ -380,6 +380,7 @@ impl Config {
.context("failed to select just added account")?;
let cfg = self
.get_account(id)
.await
.context("failed to get just added account")?;
Ok(cfg)
}
@@ -401,11 +402,11 @@ impl Config {
self.sync().await
}
fn get_account(&self, id: u32) -> Option<AccountConfig> {
async fn get_account(&self, id: u32) -> Option<AccountConfig> {
self.inner.accounts.iter().find(|e| e.id == id).cloned()
}
pub fn get_selected_account(&self) -> u32 {
pub async fn get_selected_account(&self) -> u32 {
self.inner.selected_account
}
@@ -457,7 +458,7 @@ mod tests {
let accounts2 = Accounts::open(p).await.unwrap();
assert_eq!(accounts1.accounts.len(), 1);
assert_eq!(accounts1.config.get_selected_account(), 1);
assert_eq!(accounts1.config.get_selected_account().await, 1);
assert_eq!(accounts1.dir, accounts2.dir);
assert_eq!(accounts1.config, accounts2.config,);
@@ -471,23 +472,23 @@ mod tests {
let mut accounts = Accounts::new(p.clone()).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
assert_eq!(accounts.config.get_selected_account().await, 0);
let id = accounts.add_account().await.unwrap();
assert_eq!(id, 1);
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
let id = accounts.add_account().await.unwrap();
assert_eq!(id, 2);
assert_eq!(accounts.config.get_selected_account(), id);
assert_eq!(accounts.config.get_selected_account().await, id);
assert_eq!(accounts.accounts.len(), 2);
accounts.select_account(1).await.unwrap();
assert_eq!(accounts.config.get_selected_account(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
accounts.remove_account(1).await.unwrap();
assert_eq!(accounts.config.get_selected_account(), 2);
assert_eq!(accounts.config.get_selected_account().await, 2);
assert_eq!(accounts.accounts.len(), 1);
}
@@ -497,17 +498,17 @@ mod tests {
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await?;
assert!(accounts.get_selected_account().is_none());
assert_eq!(accounts.config.get_selected_account(), 0);
assert!(accounts.get_selected_account().await.is_none());
assert_eq!(accounts.config.get_selected_account().await, 0);
let id = accounts.add_account().await?;
assert!(accounts.get_selected_account().is_some());
assert!(accounts.get_selected_account().await.is_some());
assert_eq!(id, 1);
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), id);
assert_eq!(accounts.config.get_selected_account().await, id);
accounts.remove_account(id).await?;
assert!(accounts.get_selected_account().is_none());
assert!(accounts.get_selected_account().await.is_none());
Ok(())
}
@@ -519,7 +520,7 @@ mod tests {
let mut accounts = Accounts::new(p.clone()).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
assert_eq!(accounts.config.get_selected_account().await, 0);
let extern_dbfile: PathBuf = dir.path().join("other");
let ctx = Context::new(&extern_dbfile, 0, Events::new())
@@ -536,9 +537,9 @@ mod tests {
.await
.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
let ctx = accounts.get_selected_account().unwrap();
let ctx = accounts.get_selected_account().await.unwrap();
assert_eq!(
"me@mail.com",
ctx.get_config(crate::config::Config::Addr)
@@ -561,7 +562,7 @@ mod tests {
assert_eq!(id, expected_id);
}
let ids = accounts.get_all();
let ids = accounts.get_all().await;
for (i, expected_id) in (1..10).enumerate() {
assert_eq!(ids.get(i), Some(&expected_id));
}
@@ -576,16 +577,16 @@ mod tests {
let (id0, id1, id2) = {
let mut accounts = Accounts::new(p.clone()).await?;
accounts.add_account().await?;
let ids = accounts.get_all();
let ids = accounts.get_all().await;
assert_eq!(ids.len(), 1);
let id0 = *ids.first().unwrap();
let ctx = accounts.get_account(id0).unwrap();
let id0 = *ids.get(0).unwrap();
let ctx = accounts.get_account(id0).await.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
.await?;
let id1 = accounts.add_account().await?;
let ctx = accounts.get_account(id1).unwrap();
let ctx = accounts.get_account(id1).await.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
.await?;
@@ -596,7 +597,7 @@ mod tests {
}
let id2 = accounts.add_account().await?;
let ctx = accounts.get_account(id2).unwrap();
let ctx = accounts.get_account(id2).await.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
.await?;
@@ -610,31 +611,31 @@ mod tests {
let (id0_reopened, id1_reopened, id2_reopened) = {
let accounts = Accounts::new(p.clone()).await?;
let ctx = accounts.get_selected_account().unwrap();
let ctx = accounts.get_selected_account().await.unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("two@example.org".to_string())
);
let ids = accounts.get_all();
let ids = accounts.get_all().await;
assert_eq!(ids.len(), 3);
let id0 = *ids.first().unwrap();
let ctx = accounts.get_account(id0).unwrap();
let id0 = *ids.get(0).unwrap();
let ctx = accounts.get_account(id0).await.unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("one@example.org".to_string())
);
let id1 = *ids.get(1).unwrap();
let t = accounts.get_account(id1).unwrap();
let t = accounts.get_account(id1).await.unwrap();
assert_eq!(
t.get_config(crate::config::Config::Addr).await?,
Some("two@example.org".to_string())
);
let id2 = *ids.get(2).unwrap();
let ctx = accounts.get_account(id2).unwrap();
let ctx = accounts.get_account(id2).await.unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("three@example.org".to_string())
@@ -660,7 +661,7 @@ mod tests {
assert_eq!(accounts.accounts.len(), 0);
// Create event emitter.
let event_emitter = accounts.get_event_emitter();
let event_emitter = accounts.get_event_emitter().await;
// Test that event emitter does not return `None` immediately.
let duration = std::time::Duration::from_millis(1);
@@ -691,6 +692,7 @@ mod tests {
.context("failed to add closed account")?;
let account = accounts
.get_selected_account()
.await
.context("failed to get account")?;
assert_eq!(account.id, account_id);
let passphrase_set_success = account
@@ -705,6 +707,7 @@ mod tests {
.context("failed to create second accounts manager")?;
let account = accounts
.get_selected_account()
.await
.context("failed to get account")?;
assert_eq!(account.is_open().await, false);

View File

@@ -326,7 +326,10 @@ impl<'a> BlobObject<'a> {
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
if let Some(new_name) = self.recode_to_size(context, blob_abs, img_wh, Some(20_000))? {
if let Some(new_name) = self
.recode_to_size(context, blob_abs, img_wh, Some(20_000))
.await?
{
self.name = new_name;
}
Ok(())
@@ -349,7 +352,8 @@ impl<'a> BlobObject<'a> {
};
if self
.recode_to_size(context, blob_abs, img_wh, None)?
.recode_to_size(context, blob_abs, img_wh, None)
.await?
.is_some()
{
return Err(format_err!(
@@ -359,7 +363,7 @@ impl<'a> BlobObject<'a> {
Ok(())
}
fn recode_to_size(
async fn recode_to_size(
&self,
context: &Context,
mut blob_abs: PathBuf,
@@ -734,7 +738,7 @@ mod tests {
check_image_size(avatar_src, 1000, 1000);
check_image_size(&avatar_blob, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
async fn file_size(path_buf: &Path) -> u64 {
async fn file_size(path_buf: &PathBuf) -> u64 {
let file = File::open(path_buf).await.unwrap();
file.metadata().await.unwrap().len()
}
@@ -742,6 +746,7 @@ mod tests {
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000))
.await
.unwrap();
assert!(file_size(&avatar_blob).await <= 3000);
assert!(file_size(&avatar_blob).await > 2000);

View File

@@ -40,7 +40,7 @@ use crate::webxdc::WEBXDC_SUFFIX;
use crate::{location, sql};
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum ChatItem {
Message {
msg_id: MsgId,
@@ -1129,11 +1129,6 @@ impl Chat {
&self.name
}
/// Returns mailing list address where messages are sent to.
pub fn get_mailinglist_addr(&self) -> &str {
self.param.get(Param::ListPost).unwrap_or_default()
}
/// Returns profile image path for the chat.
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
@@ -2423,7 +2418,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
pub async fn get_chat_media(
context: &Context,
chat_id: Option<ChatId>,
chat_id: ChatId,
msg_type: Viewtype,
msg_type2: Viewtype,
msg_type3: Viewtype,
@@ -2434,13 +2429,11 @@ pub async fn get_chat_media(
.query_map(
"SELECT id
FROM msgs
WHERE (1=? OR chat_id=?)
WHERE chat_id=?
AND (type=? OR type=? OR type=?)
AND hidden=0
ORDER BY timestamp, id;",
paramsv![
if chat_id.is_none() { 1i32 } else { 0i32 },
chat_id.unwrap_or_else(|| ChatId::new(0)),
chat_id,
msg_type,
if msg_type2 != Viewtype::Unknown {
msg_type2
@@ -2481,7 +2474,7 @@ pub async fn get_next_media(
if let Ok(msg) = Message::load_from_db(context, curr_msg_id).await {
let list: Vec<MsgId> = get_chat_media(
context,
Some(msg.chat_id),
msg.chat_id,
if msg_type != Viewtype::Unknown {
msg_type
} else {
@@ -2525,7 +2518,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
LEFT JOIN contacts c
ON c.id=cc.contact_id
WHERE cc.chat_id=?
ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
ORDER BY c.id=1, LOWER(c.name||c.addr), c.id;",
paramsv![chat_id],
|row| row.get::<_, ContactId>(0),
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
@@ -2788,7 +2781,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId)
Ok(needs_attach)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum MuteDuration {
NotMuted,
Forever,
@@ -2863,7 +2856,7 @@ pub async fn remove_contact_from_chat(
let mut success = false;
/* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
if !chat.is_self_in_chat(context).await? {
@@ -4554,24 +4547,26 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_protection() -> Result<()> {
async fn test_set_protection() {
let t = TestContext::new_alice().await;
t.set_config_bool(Config::BccSelf, false).await?;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
// enable protection on unpromoted chat, the info-message is added via add_info_msg()
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await?;
.await
.unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
assert!(chat.is_protected());
assert!(chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id, 0).await?;
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
assert_eq!(msgs.len(), 1);
let msg = t.get_last_msg_in(chat_id).await;
@@ -4582,9 +4577,10 @@ mod tests {
// disable protection again, still unpromoted
chat_id
.set_protection(&t, ProtectionStatus::Unprotected)
.await?;
.await
.unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
@@ -4594,20 +4590,21 @@ mod tests {
assert_eq!(msg.get_state(), MessageState::InNoticed);
// send a message, this switches to promoted state
send_text_msg(&t, chat_id, "hi!".to_string()).await?;
send_text_msg(&t, chat_id, "hi!".to_string()).await.unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
assert!(!chat.is_protected());
assert!(!chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id, 0).await?;
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
assert_eq!(msgs.len(), 3);
// enable protection on promoted chat, the info-message is sent via send_msg() this time
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
.await
.unwrap();
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
assert!(chat.is_protected());
assert!(!chat.is_unpromoted());
@@ -4615,8 +4612,6 @@ mod tests {
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -5454,8 +5449,8 @@ mod tests {
assert_eq!(
chat_id.get_encryption_info(&alice).await?,
"No encryption:\n\
fiona@example.net\n\
bob@example.net"
bob@example.net\n\
fiona@example.net"
);
let direct_chat = bob.create_chat(&alice).await;
@@ -5486,158 +5481,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_chat_media() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "bar").await?;
assert_eq!(
get_chat_media(
&t,
Some(chat_id1),
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Unknown
)
.await?
.len(),
0
);
async fn send_media(
t: &TestContext,
chat_id: ChatId,
msg_type: Viewtype,
name: &str,
bytes: &[u8],
) -> Result<MsgId> {
let file = t.get_blobdir().join(name);
tokio::fs::write(&file, bytes).await?;
let mut msg = Message::new(msg_type);
msg.set_file(file.to_str().unwrap(), None);
send_msg(t, chat_id, &mut msg).await
}
send_media(
&t,
chat_id1,
Viewtype::Image,
"a.jpg",
include_bytes!("../test-data/image/rectangle200x180-rotated.jpg"),
)
.await?;
send_media(
&t,
chat_id1,
Viewtype::Sticker,
"b.png",
include_bytes!("../test-data/image/avatar64x64.png"),
)
.await?;
send_media(
&t,
chat_id2,
Viewtype::Image,
"c.jpg",
include_bytes!("../test-data/image/avatar64x64.png"),
)
.await?;
send_media(
&t,
chat_id2,
Viewtype::Webxdc,
"d.xdc",
include_bytes!("../test-data/webxdc/minimal.xdc"),
)
.await?;
assert_eq!(
get_chat_media(
&t,
Some(chat_id1),
Viewtype::Image,
Viewtype::Unknown,
Viewtype::Unknown,
)
.await?
.len(),
1
);
assert_eq!(
get_chat_media(
&t,
Some(chat_id1),
Viewtype::Sticker,
Viewtype::Unknown,
Viewtype::Unknown,
)
.await?
.len(),
1
);
assert_eq!(
get_chat_media(
&t,
Some(chat_id1),
Viewtype::Sticker,
Viewtype::Image,
Viewtype::Unknown,
)
.await?
.len(),
2
);
assert_eq!(
get_chat_media(
&t,
Some(chat_id2),
Viewtype::Webxdc,
Viewtype::Unknown,
Viewtype::Unknown,
)
.await?
.len(),
1
);
assert_eq!(
get_chat_media(
&t,
None,
Viewtype::Image,
Viewtype::Unknown,
Viewtype::Unknown,
)
.await?
.len(),
2
);
assert_eq!(
get_chat_media(
&t,
None,
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Unknown,
)
.await?
.len(),
3
);
assert_eq!(
get_chat_media(
&t,
None,
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Webxdc,
)
.await?
.len(),
4
);
Ok(())
}
}

View File

@@ -11,7 +11,7 @@ use sha1::{Digest, Sha1};
fn str_to_angle(s: &str) -> f64 {
let bytes = s.as_bytes();
let result = Sha1::digest(bytes);
let checksum: u16 = result.first().map_or(0, |&x| u16::from(x))
let checksum: u16 = result.get(0).map_or(0, |&x| u16::from(x))
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
f64::from(checksum) / 65536.0 * 360.0
}

View File

@@ -1,7 +1,7 @@
//! # Key-value configuration management.
use anyhow::{ensure, Context as _, Result};
use strum::{EnumProperty as EnumPropertyTrait, IntoEnumIterator};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
use crate::blob::BlobObject;
@@ -55,7 +55,7 @@ pub enum Config {
Selfstatus,
Selfavatar,
#[strum(props(default = "1"))]
#[strum(props(default = "0"))]
BccSelf,
#[strum(props(default = "1"))]
@@ -198,7 +198,7 @@ impl Context {
let rel_path = self.sql.get_raw_config(key).await?;
rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(key).await?,

View File

@@ -12,11 +12,9 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use tokio::task;
use crate::config::Config;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::imap::Imap;
use crate::job;
use crate::log::LogExt;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam, Socks5Config};
use crate::message::{Message, Viewtype};
use crate::oauth2::get_oauth2_addr;
@@ -103,11 +101,35 @@ impl Context {
info!(self, "Configure ...");
let mut param = LoginParam::load_candidate_params(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
let success = configure(self, &mut param).await;
self.set_config(Config::NotifyAboutWrongPw, None).await?;
on_configure_completed(self, param, old_addr).await?;
if let Some(provider) = param.provider {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
if !self.config_exists(def.key).await? {
info!(self, "apply config_defaults {}={}", def.key, def.value);
self.set_config(def.key, Some(def.value)).await?;
} else {
info!(
self,
"skip already set config_defaults {}={}", def.key, def.value
);
}
}
}
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(provider.after_login_hint.to_string());
if chat::add_device_msg(self, Some("core-provider-info"), Some(&mut msg))
.await
.is_err()
{
warn!(self, "cannot add after_login_hint as core-provider-info");
}
}
}
success?;
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
@@ -116,54 +138,6 @@ impl Context {
}
}
async fn on_configure_completed(
context: &Context,
param: LoginParam,
old_addr: Option<String>,
) -> Result<()> {
if let Some(provider) = param.provider {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
if !context.config_exists(def.key).await? {
info!(context, "apply config_defaults {}={}", def.key, def.value);
context.set_config(def.key, Some(def.value)).await?;
} else {
info!(
context,
"skip already set config_defaults {}={}", def.key, def.value
);
}
}
}
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(provider.after_login_hint.to_string());
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
.await
.is_err()
{
warn!(context, "cannot add after_login_hint as core-provider-info");
}
}
}
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
if let Some(old_addr) = old_addr {
if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new(Viewtype::Text);
msg.text =
Some(stock_str::aeap_explanation_and_link(context, old_addr, new_addr).await);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.ok_or_log_msg(context, "Cannot add AEAP explanation");
}
}
}
Ok(())
}
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 1);
@@ -579,7 +553,8 @@ async fn try_imap_one_param(
let (_s, r) = async_channel::bounded(1);
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r).await
{
Err(err) => {
info!(context, "failure: {}", err);
return Err(ConfigurationError {
@@ -667,9 +642,6 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
.to_lowercase()
.contains("temporary failure in name resolution")
|| e.msg.to_lowercase().contains("name or service not known")
|| e.msg
.to_lowercase()
.contains("failed to lookup address information")
}) {
return stock_str::error_no_network(context).await;
}

View File

@@ -167,11 +167,6 @@ pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// String that indicates that something is left out or truncated.
pub const DC_ELLIPSIS: &str = "[...]";
// how many lines desktop can display when fullscreen (fullscreen at zoomlevel 1x)
// (taken from "subjective" testing what looks ok)
pub const DC_DESIRED_TEXT_LINES: usize = 38;
// how many chars desktop can display per line (from "subjective" testing)
pub const DC_DESIRED_TEXT_LINE_LEN: usize = 100;
/// Message length limit.
///
@@ -181,7 +176,7 @@ pub const DC_DESIRED_TEXT_LINE_LEN: usize = 100;
///
/// Note that for simplicity maximum length is defined as the number of Unicode Scalar Values (Rust
/// `char`s), not Unicode Grapheme Clusters.
pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEXT_LINES;
pub const DC_DESIRED_TEXT_LEN: usize = 5000;
// Flags for empty server job

View File

@@ -24,12 +24,9 @@ use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::sql::{self, params_iter};
use crate::tools::{get_abs_path, improve_single_line_input, time, EmailAddress};
use crate::tools::{get_abs_path, improve_single_line_input, EmailAddress};
use crate::{chat, stock_str};
/// Time during which a contact is considered as seen recently.
const SEEN_RECENTLY_SECONDS: i64 = 600;
/// Contact ID, including reserved IDs.
///
/// Some contact IDs are reserved to identify special contacts. This
@@ -326,11 +323,6 @@ impl Contact {
self.last_seen
}
/// Returns `true` if this contact was seen recently.
pub fn was_seen_recently(&self) -> bool {
time() - self.last_seen <= SEEN_RECENTLY_SECONDS
}
/// Check if a contact is blocked.
pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result<bool> {
let blocked = Self::load_from_db(context, id).await?.blocked;
@@ -716,7 +708,7 @@ impl Contact {
AND c.blocked=0 \
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) \
ORDER BY c.last_seen DESC, c.id DESC;",
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
sql::repeat_vars(self_addrs.len())
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
@@ -768,7 +760,7 @@ impl Contact {
AND id>?
AND origin>=?
AND blocked=0
ORDER BY last_seen DESC, id DESC;",
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
sql::repeat_vars(self_addrs.len())
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
@@ -857,7 +849,7 @@ impl Contact {
let list = context
.sql
.query_map(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
paramsv![ContactId::LAST_SPECIAL],
|row| row.get::<_, ContactId>(0),
|ids| {
@@ -896,8 +888,11 @@ impl Contact {
EncryptPreference::Reset => stock_str::encr_none(context).await,
};
let finger_prints = stock_str::finger_prints(context).await;
ret += &format!("{}.\n{}:", stock_message, finger_prints);
ret += &format!(
"{}.\n{}:",
stock_message,
stock_str::finger_prints(context).await
);
let fingerprint_self = SignedPublicKey::load_self(context)
.await?
@@ -1434,7 +1429,7 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
.chunks(2)
.into_iter()
.filter_map(|chunk| {
let name = chunk.first()?;
let name = chunk.get(0)?;
let addr = chunk.get(1)?;
Some((*name, *addr))
})
@@ -1449,7 +1444,7 @@ mod tests {
use crate::chatlist::Chatlist;
use crate::message::Message;
use crate::receive_imf::receive_imf;
use crate::test_utils::{self, TestContext, TestContextManager};
use crate::test_utils::{self, TestContext};
#[test]
fn test_contact_id_values() {
@@ -2275,28 +2270,4 @@ Hi."#;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_was_seen_recently() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice.create_chat(&bob).await;
let sent_msg = alice.send_text(chat.id, "moin").await;
let chat = bob.create_chat(&alice).await;
let contacts = chat::get_chat_contacts(&bob, chat.id).await?;
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
assert!(!contact.was_seen_recently());
bob.recv_msg(&sent_msg).await;
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
assert!(contact.was_seen_recently());
let self_contact = Contact::get_by_id(&bob, ContactId::SELF).await?;
assert!(!self_contact.was_seen_recently());
Ok(())
}
}

View File

@@ -138,7 +138,7 @@ impl Context {
if !blobdir.exists() {
tokio::fs::create_dir_all(&blobdir).await?;
}
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events)?;
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events).await?;
Ok(context)
}
@@ -169,7 +169,7 @@ impl Context {
self.sql.check_passphrase(passphrase).await
}
pub(crate) fn with_blobdir(
pub(crate) async fn with_blobdir(
dbfile: PathBuf,
blobdir: PathBuf,
id: u32,
@@ -193,7 +193,7 @@ impl Context {
translated_stockstrings: RwLock::new(HashMap::new()),
events,
scheduler: RwLock::new(None),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 3.0)), // Allow to send 3 messages immediately, no more than once every 20 seconds.
quota: RwLock::new(None),
server_id: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
@@ -239,17 +239,6 @@ impl Context {
}
}
/// Restarts the IO scheduler if it was running before
/// when it is not running this is an no-op
pub async fn restart_io_if_running(&self) {
info!(self, "restarting IO");
let is_running = { self.inner.scheduler.read().await.is_some() };
if is_running {
self.stop_io().await;
self.start_io().await;
}
}
/// Returns a reference to the underlying SQL instance.
///
/// Warning: this is only here for testing, not part of the public API.
@@ -346,7 +335,6 @@ impl Context {
}
}
#[allow(unused)]
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
match &*self.running_state.read().await {
RunningState::Running { .. } => false,
@@ -606,7 +594,7 @@ impl Context {
let list = if let Some(chat_id) = chat_id {
do_query(
"SELECT m.id AS id
"SELECT m.id AS id, m.timestamp AS timestamp
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
@@ -630,7 +618,7 @@ impl Context {
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
// The limit is documented and UI may add a hint when getting 1000 results.
do_query(
"SELECT m.id AS id
"SELECT m.id AS id, m.timestamp AS timestamp
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
@@ -883,7 +871,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new());
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new()).await;
assert!(res.is_err());
}
@@ -892,7 +880,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new());
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new()).await;
assert!(res.is_err());
}

View File

@@ -1,375 +0,0 @@
//! End-to-end decryption support.
use std::collections::HashSet;
use anyhow::{Context as _, Result};
use mailparse::ParsedMail;
use crate::aheader::Aheader;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::log::LogExt;
use crate::peerstate::Peerstate;
use crate::pgp;
/// Tries to decrypt a message, but only if it is structured as an
/// Autocrypt message.
///
/// Returns decrypted body and a set of valid signature fingerprints
/// if successful.
///
/// If the message is wrongly signed, this will still return the decrypted
/// message but the HashSet will be empty.
pub async fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
decryption_info: &DecryptionInfo,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
// Possibly perform decryption
let public_keyring_for_validate = keyring_from_peerstate(&decryption_info.peerstate);
let encrypted_data_part = match get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
{
None => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
}
Some(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
.await
.context("failed to get own keyring")?;
decrypt_part(
encrypted_data_part,
private_keyring,
public_keyring_for_validate,
)
.await
}
pub async fn create_decryption_info(
context: &Context,
mail: &ParsedMail<'_>,
message_time: i64,
) -> Result<DecryptionInfo> {
let from = mail
.headers
.get_header(HeaderDef::From_)
.and_then(|from_addr| mailparse::addrparse_header(from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
.unwrap_or_default();
let autocrypt_header = Aheader::from_headers(&from, &mail.headers)
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
.flatten();
let peerstate =
get_autocrypt_peerstate(context, &from, autocrypt_header.as_ref(), message_time).await?;
Ok(DecryptionInfo {
from,
autocrypt_header,
peerstate,
message_time,
})
}
#[derive(Debug)]
pub struct DecryptionInfo {
/// The From address. This is the address from the unnencrypted, outer
/// From header.
pub from: String,
pub autocrypt_header: Option<Aheader>,
/// The peerstate that will be used to validate the signatures
pub peerstate: Option<Peerstate>,
/// The timestamp when the message was sent.
/// If this is older than the peerstate's last_seen, this probably
/// means out-of-order message arrival, We don't modify the
/// peerstate in this case.
pub message_time: i64,
}
/// Returns a reference to the encrypted payload of a ["Mixed
/// Up"][pgpmime-message-mangling] message.
///
/// According to [RFC 3156] encrypted messages should have
/// `multipart/encrypted` MIME type and two parts, but Microsoft
/// Exchange and ProtonMail IMAP/SMTP Bridge are known to mangle this
/// structure by changing the type to `multipart/mixed` and prepending
/// an empty part at the start.
///
/// ProtonMail IMAP/SMTP Bridge prepends a part literally saying
/// "Empty Message", so we don't check its contents at all, checking
/// only for `text/plain` type.
///
/// Returns `None` if the message is not a "Mixed Up" message.
///
/// [RFC 3156]: https://www.rfc-editor.org/info/rfc3156
/// [pgpmime-message-mangling]: https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html
fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/mixed" {
return None;
}
if let [first_part, second_part, third_part] = &mail.subparts[..] {
if first_part.ctype.mimetype == "text/plain"
&& second_part.ctype.mimetype == "application/pgp-encrypted"
&& third_part.ctype.mimetype == "application/octet-stream"
{
Some(third_part)
} else {
None
}
} else {
None
}
}
/// Returns a reference to the encrypted payload of a message turned into attachment.
///
/// Google Workspace has an option "Append footer" which appends standard footer defined
/// by administrator to all outgoing messages. However, there is no plain text part in
/// encrypted messages sent by Delta Chat, so Google Workspace turns the message into
/// multipart/mixed MIME, where the first part is an empty plaintext part with a footer
/// and the second part is the original encrypted message.
fn get_attachment_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/mixed" {
return None;
}
if let [first_part, second_part] = &mail.subparts[..] {
if first_part.ctype.mimetype == "text/plain"
&& second_part.ctype.mimetype == "multipart/encrypted"
{
get_autocrypt_mime(second_part)
} else {
None
}
} else {
None
}
}
/// Returns a reference to the encrypted payload of a valid PGP/MIME message.
///
/// Returns `None` if the message is not a valid PGP/MIME message.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/encrypted" {
return None;
}
if let [first_part, second_part] = &mail.subparts[..] {
if first_part.ctype.mimetype == "application/pgp-encrypted"
&& second_part.ctype.mimetype == "application/octet-stream"
{
Some(second_part)
} else {
None
}
} else {
None
}
}
/// Returns Ok(None) if nothing encrypted was found.
async fn decrypt_part(
mail: &ParsedMail<'_>,
private_keyring: Keyring<SignedSecretKey>,
public_keyring_for_validate: Keyring<SignedPublicKey>,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let data = mail.get_body_raw()?;
if has_decrypted_pgp_armor(&data) {
let (plain, ret_valid_signatures) =
pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate).await?;
// Check for detached signatures.
// If decrypted part is a multipart/signed, then there is a detached signature.
let decrypted_part = mailparse::parse_mail(&plain)?;
if let Some((content, valid_detached_signatures)) =
validate_detached_signature(&decrypted_part, &public_keyring_for_validate)?
{
return Ok(Some((content, valid_detached_signatures)));
} else {
// If the message was wrongly or not signed, still return the plain text.
// The caller has to check if the signatures set is empty then.
return Ok(Some((plain, ret_valid_signatures)));
}
}
Ok(None)
}
#[allow(clippy::indexing_slicing)]
fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
if let Some(index) = input.iter().position(|b| *b > b' ') {
if input.len() - index > 26 {
let start = index;
let end = start + 27;
return &input[start..end] == b"-----BEGIN PGP MESSAGE-----";
}
}
false
}
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
///
/// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key
/// fingerprints for which there is a valid signature.
fn validate_detached_signature(
mail: &ParsedMail<'_>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
if mail.ctype.mimetype != "multipart/signed" {
return Ok(None);
}
if let [first_part, second_part] = &mail.subparts[..] {
// First part is the content, second part is the signature.
let content = first_part.raw_bytes;
let signature = second_part.get_body_raw()?;
let ret_valid_signatures =
pgp::pk_validate(content, &signature, public_keyring_for_validate)?;
Ok(Some((content.to_vec(), ret_valid_signatures)))
} else {
Ok(None)
}
}
fn keyring_from_peerstate(peerstate: &Option<Peerstate>) -> Keyring<SignedPublicKey> {
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
if let Some(ref peerstate) = *peerstate {
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.add(key.clone());
}
}
public_keyring_for_validate
}
/// Applies Autocrypt header to Autocrypt peer state and saves it into the database.
///
/// If we already know this fingerprint from another contact's peerstate, return that
/// peerstate in order to make AEAP work, but don't save it into the db yet.
///
/// Returns updated peerstate.
pub(crate) async fn get_autocrypt_peerstate(
context: &Context,
from: &str,
autocrypt_header: Option<&Aheader>,
message_time: i64,
) -> Result<Option<Peerstate>> {
let mut peerstate;
// Apply Autocrypt header
if let Some(header) = autocrypt_header {
// The "from_verified_fingerprint" part is for AEAP:
// If we know this fingerprint from another addr,
// we may want to do a transition from this other addr
// (and keep its peerstate)
// For security reasons, for now, we only do a transition
// if the fingerprint is verified.
peerstate = Peerstate::from_verified_fingerprint_or_addr(
context,
&header.public_key.fingerprint(),
from,
)
.await?;
if let Some(ref mut peerstate) = peerstate {
if addr_cmp(&peerstate.addr, from) {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
// If `peerstate.addr` and `from` differ, this means that
// someone is using the same key but a different addr, probably
// because they made an AEAP transition.
// But we don't know if that's legit until we checked the
// signatures, so wait until then with writing anything
// to the database.
} else {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
} else {
peerstate = Peerstate::from_addr(context, from).await?;
}
Ok(peerstate)
}
#[cfg(test)]
mod tests {
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use super::*;
#[test]
fn test_has_decrypted_pgp_armor() {
let data = b" -----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b" \n-----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b" -----BEGIN PGP MESSAGE---";
assert_eq!(has_decrypted_pgp_armor(data), false);
let data = b" -----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b"blas";
assert_eq!(has_decrypted_pgp_armor(data), false);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mixed_up_mime() -> Result<()> {
// "Mixed Up" mail as received when sending an encrypted
// message using Delta Chat Desktop via ProtonMail IMAP/SMTP
// Bridge.
let mixed_up_mime = include_bytes!("../test-data/message/protonmail-mixed-up.eml");
let mail = mailparse::parse_mail(mixed_up_mime)?;
assert!(get_autocrypt_mime(&mail).is_none());
assert!(get_mixed_up_mime(&mail).is_some());
assert!(get_attachment_mime(&mail).is_none());
// Same "Mixed Up" mail repaired by Thunderbird 78.9.0.
//
// It added `X-Enigmail-Info: Fixed broken PGP/MIME message`
// header although the repairing is done by the built-in
// OpenPGP support, not Enigmail.
let repaired_mime = include_bytes!("../test-data/message/protonmail-repaired.eml");
let mail = mailparse::parse_mail(repaired_mime)?;
assert!(get_autocrypt_mime(&mail).is_some());
assert!(get_mixed_up_mime(&mail).is_none());
assert!(get_attachment_mime(&mail).is_none());
// Another form of "Mixed Up" mail created by Google Workspace,
// where original message is turned into attachment to empty plaintext message.
let attachment_mime = include_bytes!("../test-data/message/google-workspace-mixed-up.eml");
let mail = mailparse::parse_mail(attachment_mime)?;
assert!(get_autocrypt_mime(&mail).is_none());
assert!(get_mixed_up_mime(&mail).is_none());
assert!(get_attachment_mime(&mail).is_some());
let bob = TestContext::new_bob().await;
receive_imf(&bob, attachment_mime, false).await?;
let msg = bob.get_last_msg().await;
assert_eq!(msg.text.as_deref(), Some("Hello from Thunderbird!"));
Ok(())
}
}

View File

@@ -11,7 +11,7 @@ use crate::imap::{Imap, ImapActionResult};
use crate::job::{self, Action, Job, Status};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::param::{Param, Params};
use crate::param::Params;
use crate::tools::time;
use crate::{job_try, stock_str, EventType};
use std::cmp::max;
@@ -69,42 +69,6 @@ impl Context {
Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
}
}
// Merges the two messages to `placeholder_msg_id`;
// `full_msg_id` is no longer used afterwards.
pub(crate) async fn merge_messages(
&self,
full_msg_id: MsgId,
placeholder_msg_id: MsgId,
) -> Result<()> {
let placeholder = Message::load_from_db(self, placeholder_msg_id).await?;
self.sql
.transaction(move |transaction| {
transaction
.execute("DELETE FROM msgs WHERE id=?;", paramsv![placeholder_msg_id])?;
transaction.execute(
"UPDATE msgs SET id=? WHERE id=?",
paramsv![placeholder_msg_id, full_msg_id],
)?;
Ok(())
})
.await?;
let mut full = Message::load_from_db(self, placeholder_msg_id).await?;
for key in [
Param::WebxdcSummary,
Param::WebxdcSummaryTimestamp,
Param::WebxdcDocument,
Param::WebxdcDocumentTimestamp,
] {
if let Some(value) = placeholder.param.get(key) {
full.param.set(key, value);
}
}
full.update_param(self).await?;
Ok(())
}
}
impl MsgId {
@@ -292,7 +256,7 @@ impl MimeMessage {
mod tests {
use num_traits::FromPrimitive;
use crate::chat::{get_chat_msgs, send_msg};
use crate::chat::send_msg;
use crate::ephemeral::Timer;
use crate::message::Viewtype;
use crate::receive_imf::receive_imf_inner;
@@ -446,119 +410,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_status_update_expands_to_nothing() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = alice.create_chat(&bob).await.id;
let file = alice.get_blobdir().join("minimal.xdc");
tokio::fs::write(&file, include_bytes!("../test-data/webxdc/minimal.xdc")).await?;
let mut instance = Message::new(Viewtype::File);
instance.set_file(file.to_str().unwrap(), None);
let _sent1 = alice.send_msg(chat_id, &mut instance).await;
alice
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#, "d")
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
let sent2_rfc742_mid = Message::load_from_db(&alice, sent2.sender_msg_id)
.await?
.rfc724_mid;
// not downloading the status update results in an placeholder
receive_imf_inner(
&bob,
&sent2_rfc742_mid,
sent2.payload().as_bytes(),
false,
Some(sent2.payload().len() as u32),
false,
)
.await?;
let msg = bob.get_last_msg().await;
let chat_id = msg.chat_id;
assert_eq!(get_chat_msgs(&bob, chat_id, 0).await?.len(), 1);
assert_eq!(msg.download_state(), DownloadState::Available);
// downloading the status update afterwards expands to nothing and moves the placeholder to trash-chat
// (usually status updates are too small for not being downloaded directly)
receive_imf_inner(
&bob,
&sent2_rfc742_mid,
sent2.payload().as_bytes(),
false,
None,
false,
)
.await?;
assert_eq!(get_chat_msgs(&bob, chat_id, 0).await?.len(), 0);
assert!(Message::load_from_db(&bob, msg.id)
.await?
.chat_id
.is_trash());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_expands_to_nothing() -> Result<()> {
let bob = TestContext::new_bob().await;
let raw = b"Subject: Message opened\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\
Message-ID: <bar@example.org>\n\
To: Alice <alice@example.org>\n\
From: Bob <bob@example.org>\n\
Content-Type: multipart/report; report-type=disposition-notification;\n\t\
boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
Content-Type: text/plain; charset=utf-8\n\
\n\
bla\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
Content-Type: message/disposition-notification\n\
\n\
Reporting-UA: Delta Chat 1.88.0\n\
Original-Recipient: rfc822;bob@example.org\n\
Final-Recipient: rfc822;bob@example.org\n\
Original-Message-ID: <foo@example.org>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n\
\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
";
// not downloading the mdn results in an placeholder
receive_imf_inner(
&bob,
"bar@example.org",
raw,
false,
Some(raw.len() as u32),
false,
)
.await?;
let msg = bob.get_last_msg().await;
let chat_id = msg.chat_id;
assert_eq!(get_chat_msgs(&bob, chat_id, 0).await?.len(), 1);
assert_eq!(msg.download_state(), DownloadState::Available);
// downloading the mdn afterwards expands to nothing and deletes the placeholder directly
// (usually mdn are too small for not being downloaded directly)
receive_imf_inner(&bob, "bar@example.org", raw, false, None, false).await?;
assert_eq!(get_chat_msgs(&bob, chat_id, 0).await?.len(), 0);
assert!(Message::load_from_db(&bob, msg.id)
.await?
.chat_id
.is_trash());
Ok(())
}
}

View File

@@ -1,13 +1,20 @@
//! End-to-end encryption support.
use std::collections::HashSet;
use anyhow::{format_err, Context as _, Result};
use mailparse::ParsedMail;
use num_traits::FromPrimitive;
use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::key::{DcKey, SignedPublicKey, SignedSecretKey};
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::log::LogExt;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::pgp;
@@ -126,6 +133,299 @@ impl EncryptHelper {
}
}
/// Applies Autocrypt header to Autocrypt peer state and saves it into the database.
///
/// If we already know this fingerprint from another contact's peerstate, return that
/// peerstate in order to make AEAP work, but don't save it into the db yet.
///
/// Returns updated peerstate.
pub(crate) async fn get_autocrypt_peerstate(
context: &Context,
from: &str,
autocrypt_header: Option<&Aheader>,
message_time: i64,
) -> Result<Option<Peerstate>> {
let mut peerstate;
// Apply Autocrypt header
if let Some(header) = autocrypt_header {
// The "from_nongossiped_fingerprint" part is for AEAP:
// If we know this fingerprint from another addr,
// we may want to do a transition from this other addr
// (and keep its peerstate)
peerstate = Peerstate::from_nongossiped_fingerprint_or_addr(
context,
&header.public_key.fingerprint(),
from,
)
.await?;
if let Some(ref mut peerstate) = peerstate {
if addr_cmp(&peerstate.addr, from) {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
// If `peerstate.addr` and `from` differ, this means that
// someone is using the same key but a different addr, probably
// because they made an AEAP transition.
// But we don't know if that's legit until we checked the
// signatures, so wait until then with writing anything
// to the database.
} else {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
} else {
peerstate = Peerstate::from_addr(context, from).await?;
}
Ok(peerstate)
}
/// Tries to decrypt a message, but only if it is structured as an
/// Autocrypt message.
///
/// Returns decrypted body and a set of valid signature fingerprints
/// if successful.
///
/// If the message is wrongly signed, this will still return the decrypted
/// message but the HashSet will be empty.
pub async fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
decryption_info: &DecryptionInfo,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
// Possibly perform decryption
let public_keyring_for_validate = keyring_from_peerstate(&decryption_info.peerstate);
let context = context;
let encrypted_data_part = match get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
{
None => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
}
Some(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
.await
.context("failed to get own keyring")?;
decrypt_part(
encrypted_data_part,
private_keyring,
public_keyring_for_validate,
)
.await
}
pub async fn create_decryption_info(
context: &Context,
mail: &ParsedMail<'_>,
message_time: i64,
) -> Result<DecryptionInfo> {
let from = mail
.headers
.get_header(HeaderDef::From_)
.and_then(|from_addr| mailparse::addrparse_header(from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
.unwrap_or_default();
let autocrypt_header = Aheader::from_headers(&from, &mail.headers)
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
.flatten();
let peerstate =
get_autocrypt_peerstate(context, &from, autocrypt_header.as_ref(), message_time).await?;
Ok(DecryptionInfo {
from,
autocrypt_header,
peerstate,
message_time,
})
}
#[derive(Debug)]
pub struct DecryptionInfo {
/// The From address. This is the address from the unnencrypted, outer
/// From header.
pub from: String,
pub autocrypt_header: Option<Aheader>,
/// The peerstate that will be used to validate the signatures
pub peerstate: Option<Peerstate>,
/// The timestamp when the message was sent.
/// If this is older than the peerstate's last_seen, this probably
/// means out-of-order message arrival, We don't modify the
/// peerstate in this case.
pub message_time: i64,
}
/// Returns a reference to the encrypted payload of a valid PGP/MIME message.
///
/// Returns `None` if the message is not a valid PGP/MIME message.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/encrypted" {
return None;
}
if let [first_part, second_part] = &mail.subparts[..] {
if first_part.ctype.mimetype == "application/pgp-encrypted"
&& second_part.ctype.mimetype == "application/octet-stream"
{
Some(second_part)
} else {
None
}
} else {
None
}
}
/// Returns a reference to the encrypted payload of a ["Mixed
/// Up"][pgpmime-message-mangling] message.
///
/// According to [RFC 3156] encrypted messages should have
/// `multipart/encrypted` MIME type and two parts, but Microsoft
/// Exchange and ProtonMail IMAP/SMTP Bridge are known to mangle this
/// structure by changing the type to `multipart/mixed` and prepending
/// an empty part at the start.
///
/// ProtonMail IMAP/SMTP Bridge prepends a part literally saying
/// "Empty Message", so we don't check its contents at all, checking
/// only for `text/plain` type.
///
/// Returns `None` if the message is not a "Mixed Up" message.
///
/// [RFC 3156]: https://www.rfc-editor.org/info/rfc3156
/// [pgpmime-message-mangling]: https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html
fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/mixed" {
return None;
}
if let [first_part, second_part, third_part] = &mail.subparts[..] {
if first_part.ctype.mimetype == "text/plain"
&& second_part.ctype.mimetype == "application/pgp-encrypted"
&& third_part.ctype.mimetype == "application/octet-stream"
{
Some(third_part)
} else {
None
}
} else {
None
}
}
/// Returns a reference to the encrypted payload of a message turned into attachment.
///
/// Google Workspace has an option "Append footer" which appends standard footer defined
/// by administrator to all outgoing messages. However, there is no plain text part in
/// encrypted messages sent by Delta Chat, so Google Workspace turns the message into
/// multipart/mixed MIME, where the first part is an empty plaintext part with a footer
/// and the second part is the original encrypted message.
fn get_attachment_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/mixed" {
return None;
}
if let [first_part, second_part] = &mail.subparts[..] {
if first_part.ctype.mimetype == "text/plain"
&& second_part.ctype.mimetype == "multipart/encrypted"
{
get_autocrypt_mime(second_part)
} else {
None
}
} else {
None
}
}
fn keyring_from_peerstate(peerstate: &Option<Peerstate>) -> Keyring<SignedPublicKey> {
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
if let Some(ref peerstate) = *peerstate {
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.add(key.clone());
}
}
public_keyring_for_validate
}
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
///
/// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key
/// fingerprints for which there is a valid signature.
fn validate_detached_signature(
mail: &ParsedMail<'_>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
if mail.ctype.mimetype != "multipart/signed" {
return Ok(None);
}
if let [first_part, second_part] = &mail.subparts[..] {
// First part is the content, second part is the signature.
let content = first_part.raw_bytes;
let signature = second_part.get_body_raw()?;
let ret_valid_signatures =
pgp::pk_validate(content, &signature, public_keyring_for_validate)?;
Ok(Some((content.to_vec(), ret_valid_signatures)))
} else {
Ok(None)
}
}
/// Returns Ok(None) if nothing encrypted was found.
async fn decrypt_part(
mail: &ParsedMail<'_>,
private_keyring: Keyring<SignedSecretKey>,
public_keyring_for_validate: Keyring<SignedPublicKey>,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let data = mail.get_body_raw()?;
if has_decrypted_pgp_armor(&data) {
let (plain, ret_valid_signatures) =
pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate).await?;
// Check for detached signatures.
// If decrypted part is a multipart/signed, then there is a detached signature.
let decrypted_part = mailparse::parse_mail(&plain)?;
if let Some((content, valid_detached_signatures)) =
validate_detached_signature(&decrypted_part, &public_keyring_for_validate)?
{
return Ok(Some((content, valid_detached_signatures)));
} else {
// If the message was wrongly or not signed, still return the plain text.
// The caller has to check if the signatures set is empty then.
return Ok(Some((plain, ret_valid_signatures)));
}
}
Ok(None)
}
#[allow(clippy::indexing_slicing)]
fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
if let Some(index) = input.iter().position(|b| *b > b' ') {
if input.len() - index > 26 {
let start = index;
let end = start + 27;
return &input[start..end] == b"-----BEGIN PGP MESSAGE-----";
}
}
false
}
/// Ensures a private key exists for the configured user.
///
/// Normally the private key is generated when the first message is
@@ -148,6 +448,7 @@ mod tests {
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::peerstate::ToSave;
use crate::receive_imf::receive_imf;
use crate::test_utils::{bob_keypair, TestContext};
use super::*;
@@ -195,6 +496,24 @@ Sent with my Delta Chat Messenger: https://delta.chat";
);
}
#[test]
fn test_has_decrypted_pgp_armor() {
let data = b" -----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b" \n-----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b" -----BEGIN PGP MESSAGE---";
assert_eq!(has_decrypted_pgp_armor(data), false);
let data = b" -----BEGIN PGP MESSAGE-----";
assert_eq!(has_decrypted_pgp_armor(data), true);
let data = b"blas";
assert_eq!(has_decrypted_pgp_armor(data), false);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypted_no_autocrypt() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
@@ -329,4 +648,42 @@ Sent with my Delta Chat Messenger: https://delta.chat";
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mixed_up_mime() -> Result<()> {
// "Mixed Up" mail as received when sending an encrypted
// message using Delta Chat Desktop via ProtonMail IMAP/SMTP
// Bridge.
let mixed_up_mime = include_bytes!("../test-data/message/protonmail-mixed-up.eml");
let mail = mailparse::parse_mail(mixed_up_mime)?;
assert!(get_autocrypt_mime(&mail).is_none());
assert!(get_mixed_up_mime(&mail).is_some());
assert!(get_attachment_mime(&mail).is_none());
// Same "Mixed Up" mail repaired by Thunderbird 78.9.0.
//
// It added `X-Enigmail-Info: Fixed broken PGP/MIME message`
// header although the repairing is done by the built-in
// OpenPGP support, not Enigmail.
let repaired_mime = include_bytes!("../test-data/message/protonmail-repaired.eml");
let mail = mailparse::parse_mail(repaired_mime)?;
assert!(get_autocrypt_mime(&mail).is_some());
assert!(get_mixed_up_mime(&mail).is_none());
assert!(get_attachment_mime(&mail).is_none());
// Another form of "Mixed Up" mail created by Google Workspace,
// where original message is turned into attachment to empty plaintext message.
let attachment_mime = include_bytes!("../test-data/message/google-workspace-mixed-up.eml");
let mail = mailparse::parse_mail(attachment_mime)?;
assert!(get_autocrypt_mime(&mail).is_none());
assert!(get_mixed_up_mime(&mail).is_none());
assert!(get_attachment_mime(&mail).is_some());
let bob = TestContext::new_bob().await;
receive_imf(&bob, attachment_mime, false).await?;
let msg = bob.get_last_msg().await;
assert_eq!(msg.text.as_deref(), Some("Hello from Thunderbird!"));
Ok(())
}
}

View File

@@ -36,21 +36,15 @@
//!
//! ## How messages are deleted
//!
//! When Delta Chat deletes the message locally, it moves the message
//! to the trash chat and removes actual message contents. Messages in
//! the trash chat are called "tombstones" and track the Message-ID to
//! prevent accidental redownloading of the message from the server,
//! e.g. in case of UID validity change.
//!
//! Vice versa, when Delta Chat deletes the message from the server,
//! it removes IMAP folder and UID row from the `imap` table, but
//! keeps the message in the `msgs` table.
//!
//! Delta Chat eventually removes tombstones from the `msgs` table,
//! leaving no trace of the message, when it thinks there are no more
//! copies of the message stored on the server, i.e. when there is no
//! corresponding `imap` table entry. This is done in the
//! `prune_tombstones()` procedure during housekeeping.
//! When the message is deleted locally, its contents is removed and
//! it is moved to the trash chat. This database entry is then used to
//! track the Message-ID and corresponding IMAP folder and UID until
//! the message is deleted from the server. Vice versa, when device
//! deletes the message from the server, it removes IMAP folder and
//! UID information, but keeps the message contents. When database
//! entry is both moved to trash chat and does not contain UID
//! information, it is deleted from the database, leaving no trace of
//! the message.
//!
//! ## When messages are deleted
//!
@@ -332,35 +326,35 @@ pub(crate) async fn start_ephemeral_timers_msgids(
Ok(())
}
/// Selects messages which are expired according to
/// Deletes messages which are expired according to
/// `delete_device_after` setting or `ephemeral_timestamp` column.
///
/// For each message a row ID, chat id and viewtype is returned.
async fn select_expired_messages(
context: &Context,
now: i64,
) -> Result<Vec<(MsgId, ChatId, Viewtype)>> {
let mut rows = context
/// Returns true if any message is deleted, so caller can emit
/// MsgsChanged event. If nothing has been deleted, returns
/// false. This function does not emit the MsgsChanged event itself,
/// because it is also called when chatlist is reloaded, and emitting
/// MsgsChanged there will cause infinite reload loop.
pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Result<()> {
let mut updated = context
.sql
.query_map(
.execute(
// If you change which information is removed here, also change MsgId::trash() and
// which information receive_imf::add_parts() still adds to the db if the chat_id is TRASH
r#"
SELECT id, chat_id, type
FROM msgs
UPDATE msgs
SET
chat_id=?, txt='', subject='', txt_raw='',
mime_headers='', from_id=0, to_id=0, param=''
WHERE
ephemeral_timestamp != 0
AND ephemeral_timestamp <= ?
AND chat_id != ?
"#,
paramsv![now, DC_CHAT_ID_TRASH],
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let viewtype: Viewtype = row.get("type")?;
Ok((id, chat_id, viewtype))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
paramsv![DC_CHAT_ID_TRASH, now, DC_CHAT_ID_TRASH],
)
.await?;
.await
.context("update failed")?
> 0;
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
let self_chat_id = ChatId::lookup_by_contact(context, ContactId::SELF)
@@ -372,81 +366,36 @@ WHERE
let threshold_timestamp = now.saturating_sub(delete_device_after);
let rows_expired = context
// Delete expired messages
//
// Only update the rows that have to be updated, to avoid emitting
// unnecessary "chat modified" events.
let rows_modified = context
.sql
.query_map(
r#"
SELECT id, chat_id, type
FROM msgs
WHERE
timestamp < ?
AND chat_id > ?
AND chat_id != ?
AND chat_id != ?
"#,
.execute(
"UPDATE msgs \
SET chat_id = ?, txt = '', subject='', txt_raw='', \
mime_headers='', from_id=0, to_id=0, param='' \
WHERE timestamp < ? \
AND chat_id > ? \
AND chat_id != ? \
AND chat_id != ?",
paramsv![
DC_CHAT_ID_TRASH,
threshold_timestamp,
DC_CHAT_ID_LAST_SPECIAL,
self_chat_id,
device_chat_id
],
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let viewtype: Viewtype = row.get("type")?;
Ok((id, chat_id, viewtype))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
rows.extend(rows_expired);
}
Ok(rows)
}
/// Deletes messages which are expired according to
/// `delete_device_after` setting or `ephemeral_timestamp` column.
///
/// Emits relevant `MsgsChanged` and `WebxdcInstanceDeleted` events
/// if messages are deleted.
pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Result<()> {
let rows = select_expired_messages(context, now).await?;
if !rows.is_empty() {
context
.sql
.execute(
// If you change which information is removed here, also change MsgId::trash() and
// which information receive_imf::add_parts() still adds to the db if the chat_id is TRASH
&format!(
r#"
UPDATE msgs
SET
chat_id=?, txt='', subject='', txt_raw='',
mime_headers='', from_id=0, to_id=0, param=''
WHERE id IN ({})
"#,
sql::repeat_vars(rows.len())
),
rusqlite::params_from_iter(
std::iter::once(&DC_CHAT_ID_TRASH as &dyn crate::ToSql).chain(
rows.iter()
.map(|(msg_id, _chat_id, _viewtype)| msg_id as &dyn crate::ToSql),
),
),
)
.await
.context("update failed")?;
.context("deleted update failed")?;
for (msg_id, chat_id, viewtype) in rows {
context.emit_msgs_changed(chat_id, msg_id);
updated |= rows_modified > 0;
}
if viewtype == Viewtype::Webxdc {
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id });
}
}
if updated {
context.emit_msgs_changed_without_ids();
}
Ok(())
@@ -638,7 +587,7 @@ mod tests {
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await,
"You disabled message deletion timer."
"Message deletion timer is disabled by me."
);
assert_eq!(
@@ -648,7 +597,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 1 s."
"Message deletion timer is set to 1 s by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -657,7 +606,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 30 s."
"Message deletion timer is set to 30 s by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -666,7 +615,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 1 minute."
"Message deletion timer is set to 1 minute by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -675,7 +624,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 1.5 minutes."
"Message deletion timer is set to 1.5 minutes by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -684,7 +633,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 30 minutes."
"Message deletion timer is set to 30 minutes by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -693,7 +642,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 1 hour."
"Message deletion timer is set to 1 hour by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -702,7 +651,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 1.5 hours."
"Message deletion timer is set to 1.5 hours by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -713,7 +662,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 2 hours."
"Message deletion timer is set to 2 hours by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -724,7 +673,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 1 day."
"Message deletion timer is set to 1 day by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -735,7 +684,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 2 days."
"Message deletion timer is set to 2 days by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -746,7 +695,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 1 week."
"Message deletion timer is set to 1 week by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -757,7 +706,7 @@ mod tests {
ContactId::SELF
)
.await,
"You set message deletion timer to 4 weeks."
"Message deletion timer is set to 4 weeks by me."
);
}
@@ -1001,19 +950,6 @@ mod tests {
assert!(next_expiration < deleted_at);
delete_expired_messages(t, deleted_at).await?;
t.evtracker
.get_matching(|evt| {
if let EventType::MsgsChanged {
msg_id: event_msg_id,
..
} = evt
{
*event_msg_id == msg_id
} else {
false
}
})
.await;
let loaded = Message::load_from_db(t, msg_id).await?;
assert_eq!(loaded.text.unwrap(), "");

View File

@@ -302,9 +302,4 @@ pub enum EventType {
msg_id: MsgId,
status_update_serial: StatusUpdateSerial,
},
/// Inform that a message containing a webxdc instance has been deleted
WebxdcInstanceDeleted {
msg_id: MsgId,
},
}

View File

@@ -98,7 +98,7 @@ impl HtmlMsgParser {
if parser.html.is_empty() {
if let Some(plain) = &parser.plain {
parser.html = plain.to_html();
parser.html = plain.to_html().await;
}
} else {
parser.cid_to_data_recursive(context, &parsedmail).await?;

View File

@@ -237,7 +237,7 @@ impl Imap {
/// Creates new disconnected IMAP client using the specific login parameters.
///
/// `addr` is used to renew token if OAuth2 authentication is used.
pub fn new(
pub async fn new(
lp: &ServerLoginParam,
socks5_config: Option<Socks5Config>,
addr: &str,
@@ -303,7 +303,8 @@ impl Imap {
provider.strict_tls
}),
idle_interrupt,
)?;
)
.await?;
Ok(imap)
}

View File

@@ -134,9 +134,19 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
}
/// Initiates key transfer via Autocrypt Setup Message.
///
/// Returns setup code.
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
use futures::future::FutureExt;
let cancel = context.alloc_ongoing().await?;
let res = do_initiate_key_transfer(context)
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
.await;
context.free_ongoing().await;
res
}
async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
let setup_code = create_setup_code(context);
/* this may require a keypair to be created. this may take a second ... */
let setup_file_content = render_setup_file(context, &setup_code).await?;
@@ -161,7 +171,17 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
msg.force_plaintext();
msg.param.set_int(Param::SkipAutocrypt, 1);
chat::send_msg(context, chat_id, &mut msg).await?;
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
info!(context, "Wait for setup message being sent ...",);
while !context.shall_stop_ongoing().await {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
if let Ok(msg) = Message::load_from_db(context, msg_id).await {
if msg.is_sent() {
info!(context, "... setup message sent.",);
break;
}
}
}
// no maybe_add_bcc_self_device_msg() here.
// the ui shows the dialog with the setup code on this device,
// it would be too much noise to have two things popping up at the same time.
@@ -960,10 +980,16 @@ mod tests {
async fn test_key_transfer() -> Result<()> {
let alice = TestContext::new_alice().await;
let setup_code = initiate_key_transfer(&alice).await?;
let alice_clone = alice.clone();
let key_transfer_task = tokio::task::spawn(async move {
let ctx = alice_clone;
initiate_key_transfer(&ctx).await
});
// Get Autocrypt Setup Message.
// Wait for the message to be added to the queue.
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let sent = alice.pop_sent_msg().await;
let setup_code = key_transfer_task.await??;
// Alice sets up a second device.
let alice2 = TestContext::new().await;

View File

@@ -69,7 +69,7 @@ pub enum Action {
ResyncFolders = 300,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Job {
pub job_id: u32,
pub action: Action,

View File

@@ -15,7 +15,7 @@
)]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,
clippy::eval_order_dependence,
clippy::bool_assert_comparison,
clippy::manual_split_once,
clippy::format_push_string
@@ -58,7 +58,6 @@ mod configure;
pub mod constants;
pub mod contact;
pub mod context;
mod decrypt;
pub mod download;
mod e2ee;
pub mod ephemeral;

View File

@@ -15,7 +15,7 @@ use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::provider::{get_provider_by_id, Provider};
use crate::{context::Context, provider::Socket};
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub enum CertificateChecks {
@@ -39,7 +39,7 @@ impl Default for CertificateChecks {
}
/// Login parameters for a single server, either IMAP or SMTP
#[derive(Default, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Debug, Clone, PartialEq)]
pub struct ServerLoginParam {
pub server: String,
pub user: String,
@@ -53,7 +53,7 @@ pub struct ServerLoginParam {
pub certificate_checks: CertificateChecks,
}
#[derive(Default, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Socks5Config {
pub host: String,
pub port: u16,
@@ -128,7 +128,7 @@ impl fmt::Display for Socks5Config {
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Debug, Clone, PartialEq)]
pub struct LoginParam {
pub addr: String,
pub imap: ServerLoginParam,

View File

@@ -19,7 +19,7 @@ use crate::download::DownloadState;
use crate::ephemeral::{start_ephemeral_timers_msgids, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::imap::markseen_on_imap_table;
use crate::mimeparser::{parse_message_id, DeliveryReport, SystemMessage};
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::scheduler::InterruptInfo;
@@ -1236,11 +1236,6 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
.trash(context)
.await
.with_context(|| format!("Unable to trash message {}", msg_id))?;
if msg.viewtype == Viewtype::Webxdc {
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: *msg_id });
}
context
.sql
.execute(
@@ -1537,7 +1532,7 @@ pub async fn handle_mdn(
/// Where appropriate, also adds an info message telling the user which of the recipients of a group message failed.
pub(crate) async fn handle_ndn(
context: &Context,
failed: &DeliveryReport,
failed: &FailureReport,
error: Option<String>,
) -> Result<()> {
if failed.rfc724_mid.is_empty() {
@@ -1593,7 +1588,7 @@ pub(crate) async fn handle_ndn(
async fn ndn_maybe_add_info_msg(
context: &Context,
failed: &DeliveryReport,
failed: &FailureReport,
chat_id: ChatId,
chat_type: Chattype,
) -> Result<()> {

View File

@@ -445,8 +445,6 @@ impl<'a> MimeFactory<'a> {
.collect()
}
/// Consumes a `MimeFactory` and renders it into a message which is then stored in
/// `smtp`-table to be used by the SMTP loop
pub async fn render(mut self, context: &Context) -> Result<RenderedEmail> {
let mut headers: MessageHeaders = Default::default();
@@ -1177,17 +1175,17 @@ impl<'a> MimeFactory<'a> {
if command == SystemMessage::MultiDeviceSync && self.is_e2ee_guaranteed() {
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
let ids = self.msg.param.get(Param::Arg2).unwrap_or_default();
parts.push(context.build_sync_part(json.to_string()));
parts.push(context.build_sync_part(json.to_string()).await);
self.sync_ids_to_delete = Some(ids.to_string());
} else if command == SystemMessage::WebxdcStatusUpdate {
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
parts.push(context.build_status_update_part(json));
parts.push(context.build_status_update_part(json).await);
} else if self.msg.viewtype == Viewtype::Webxdc {
if let Some(json) = context
.render_webxdc_status_update_object(self.msg.id, None)
.await?
{
parts.push(context.build_status_update_part(&json));
parts.push(context.build_status_update_part(&json).await);
}
}

View File

@@ -12,11 +12,11 @@ use once_cell::sync::Lazy;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN};
use crate::constants::{DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
use crate::contact::{addr_cmp, addr_normalize, ContactId};
use crate::context::Context;
use crate::decrypt::{create_decryption_info, try_decrypt};
use crate::dehtml::dehtml;
use crate::e2ee;
use crate::events::EventType;
use crate::format_flowed::unformat_flowed;
use crate::headerdef::{HeaderDef, HeaderDefMap};
@@ -28,7 +28,7 @@ use crate::peerstate::Peerstate;
use crate::simplify::{simplify, SimplifiedText};
use crate::stock_str;
use crate::sync::SyncItems;
use crate::tools::{get_filemeta, parse_receive_headers, truncate_by_lines};
use crate::tools::{get_filemeta, parse_receive_headers, truncate};
/// A parsed MIME message.
///
@@ -73,7 +73,7 @@ pub struct MimeMessage {
pub(crate) user_avatar: Option<AvatarAction>,
pub(crate) group_avatar: Option<AvatarAction>,
pub(crate) mdn_reports: Vec<Report>,
pub(crate) delivery_report: Option<DeliveryReport>,
pub(crate) failure_report: Option<FailureReport>,
/// Standard USENET signature, if any.
pub(crate) footer: Option<String>,
@@ -220,11 +220,12 @@ impl MimeMessage {
let mut mail_raw = Vec::new();
let mut gossiped_addr = Default::default();
let mut from_is_signed = false;
let mut decryption_info = create_decryption_info(context, &mail, message_time).await?;
let mut decryption_info =
e2ee::create_decryption_info(context, &mail, message_time).await?;
// `signatures` is non-empty exactly if the message was encrypted and correctly signed.
let (mail, signatures, warn_empty_signature) =
match try_decrypt(context, &mail, &decryption_info).await {
match e2ee::try_decrypt(context, &mail, &decryption_info).await {
Ok(Some((raw, signatures))) => {
// Encrypted, but maybe unsigned message. Only if
// `signatures` set is non-empty, it is a valid
@@ -331,7 +332,7 @@ impl MimeMessage {
webxdc_status_update: None,
user_avatar: None,
group_avatar: None,
delivery_report: None,
failure_report: None,
footer: None,
is_mime_modified: false,
decoded_data: Vec::new(),
@@ -392,10 +393,15 @@ impl MimeMessage {
/// Parses system messages.
fn parse_system_message_headers(&mut self, context: &Context) {
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() {
self.parts.retain(|part| {
part.mimetype.is_none()
|| part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
});
self.parts = self
.parts
.iter()
.filter(|part| {
part.mimetype.is_none()
|| part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
})
.cloned()
.collect();
if self.parts.len() == 1 {
self.is_system_message = SystemMessage::AutocryptSetupMessage;
@@ -411,8 +417,6 @@ impl MimeMessage {
self.is_system_message = SystemMessage::ChatProtectionEnabled;
} else if value == "protection-disabled" {
self.is_system_message = SystemMessage::ChatProtectionDisabled;
} else if value == "group-avatar-changed" {
self.is_system_message = SystemMessage::GroupImageChanged;
}
} else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
self.is_system_message = SystemMessage::MemberRemovedFromGroup;
@@ -420,6 +424,10 @@ impl MimeMessage {
self.is_system_message = SystemMessage::MemberAddedToGroup;
} else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
self.is_system_message = SystemMessage::GroupNameChanged;
} else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
self.is_system_message = SystemMessage::GroupImageChanged;
}
}
}
@@ -527,9 +535,7 @@ impl MimeMessage {
self.parse_system_message_headers(context);
self.parse_avatar_headers(context).await;
self.parse_videochat_headers();
if self.delivery_report.is_none() {
self.squash_attachment_parts();
}
self.squash_attachment_parts();
if let Some(ref subject) = self.get_subject() {
let mut prepend_subject = true;
@@ -546,7 +552,7 @@ impl MimeMessage {
// For mailing lists, always add the subject because sometimes there are different topics
// and otherwise it might be hard to keep track:
if self.is_mailinglist_message() && !self.has_chat_version() {
if self.is_mailinglist_message() {
prepend_subject = true;
}
@@ -741,7 +747,7 @@ impl MimeMessage {
MimeS::Single
}
} else if mimetype.starts_with("message") {
if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
if mimetype == "message/rfc822" {
MimeS::Message
} else {
MimeS::Single
@@ -861,7 +867,7 @@ impl MimeMessage {
// Some providers, e.g. Tiscali, forget to set the report-type. So, if it's None, assume that it might be delivery-status
Some("delivery-status") | None => {
if let Some(report) = self.process_delivery_status(context, mail)? {
self.delivery_report = Some(report);
self.failure_report = Some(report);
}
// Add all parts (we need another part, preferably text/plain, to show as an error message)
@@ -1007,15 +1013,14 @@ impl MimeMessage {
(simplified_txt, top_quote)
};
// Truncate text if it has too many lines
let (simplified_txt, was_truncated) = truncate_by_lines(
simplified_txt,
DC_DESIRED_TEXT_LINES,
DC_DESIRED_TEXT_LINE_LEN,
);
if was_truncated {
self.is_mime_modified = was_truncated;
}
let simplified_txt = if simplified_txt.chars().count()
> DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len()
{
self.is_mime_modified = true;
truncate(&*simplified_txt, DC_DESIRED_TEXT_LEN).to_string()
} else {
simplified_txt
};
if !simplified_txt.is_empty() || simplified_quote.is_some() {
let mut part = Part {
@@ -1273,46 +1278,9 @@ impl MimeMessage {
&self,
context: &Context,
report: &mailparse::ParsedMail<'_>,
) -> Result<Option<DeliveryReport>> {
// Assume failure.
let mut failure = true;
if let Some(status_part) = report.subparts.get(1) {
// RFC 3464 defines `message/delivery-status`
// RFC 6533 defines `message/global-delivery-status`
if status_part.ctype.mimetype != "message/delivery-status"
&& status_part.ctype.mimetype != "message/global-delivery-status"
{
warn!(context, "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring");
return Ok(None);
}
let status_body = status_part.get_body_raw()?;
// Skip per-message fields.
let (_, sz) = mailparse::parse_headers(&status_body)?;
// Parse first set of per-recipient fields
if let Some(status_body) = status_body.get(sz..) {
let (status_fields, _) = mailparse::parse_headers(status_body)?;
if let Some(action) = status_fields.get_first_value("action") {
if action != "failed" {
info!(context, "DSN with {:?} action", action);
failure = false;
}
} else {
warn!(context, "DSN without action");
}
} else {
warn!(context, "DSN without per-recipient fields");
}
} else {
// No message/delivery-status part.
return Ok(None);
}
) -> Result<Option<FailureReport>> {
// parse as mailheaders
if let Some(original_msg) = report.subparts.get(2).filter(|p| {
if let Some(original_msg) = report.subparts.iter().find(|p| {
p.ctype.mimetype.contains("rfc822")
|| p.ctype.mimetype == "message/global"
|| p.ctype.mimetype == "message/global-headers"
@@ -1333,10 +1301,9 @@ impl MimeMessage {
None // We do not know which recipient failed
};
return Ok(Some(DeliveryReport {
return Ok(Some(FailureReport {
rfc724_mid: original_message_id,
failed_recipient: to.map(|s| s.addr),
failure,
}));
}
@@ -1417,7 +1384,7 @@ impl MimeMessage {
} else {
false
};
if maybe_ndn && self.delivery_report.is_none() {
if maybe_ndn && self.failure_report.is_none() {
static RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"Message-ID:(.*)").unwrap());
for captures in self
@@ -1431,10 +1398,9 @@ impl MimeMessage {
if let Ok(Some(_)) =
message::rfc724_mid_exists(context, &original_message_id).await
{
self.delivery_report = Some(DeliveryReport {
self.failure_report = Some(FailureReport {
rfc724_mid: original_message_id,
failed_recipient: None,
failure: true,
})
}
}
@@ -1472,15 +1438,13 @@ impl MimeMessage {
}
}
if let Some(delivery_report) = &self.delivery_report {
if delivery_report.failure {
let error = parts
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
if let Err(e) = message::handle_ndn(context, delivery_report, error).await {
warn!(context, "Could not handle ndn: {}", e);
}
if let Some(failure_report) = &self.failure_report {
let error = parts
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
if let Err(e) = message::handle_ndn(context, failure_report, error).await {
warn!(context, "Could not handle ndn: {}", e);
}
}
}
@@ -1560,7 +1524,6 @@ async fn update_gossip_peerstates(
Ok(gossiped_addr)
}
/// Message Disposition Notification (RFC 8098)
#[derive(Debug)]
pub(crate) struct Report {
/// Original-Message-ID header
@@ -1572,12 +1535,10 @@ pub(crate) struct Report {
additional_message_ids: Vec<String>,
}
/// Delivery Status Notification (RFC 3464, RFC 6533)
#[derive(Debug)]
pub(crate) struct DeliveryReport {
pub(crate) struct FailureReport {
pub rfc724_mid: String,
pub failed_recipient: Option<String>,
pub failure: bool,
}
#[allow(clippy::indexing_slicing)]
@@ -1670,18 +1631,14 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
mime::VIDEO => Viewtype::Video,
mime::MULTIPART => Viewtype::Unknown,
mime::MESSAGE => {
if is_attachment_disposition(mail) {
Viewtype::File
} else {
// Enacapsulated messages, see <https://www.w3.org/Protocols/rfc1341/7_3_Message.html>
// Also used as part "message/disposition-notification" of "multipart/report", which, however, will
// be handled separatedly.
// I've not seen any messages using this, so we do not attach these parts (maybe they're used to attach replies,
// which are unwanted at all).
// For now, we skip these parts at all; if desired, we could return DcMimeType::File/DC_MSG_File
// for selected and known subparts.
Viewtype::Unknown
}
// Enacapsulated messages, see <https://www.w3.org/Protocols/rfc1341/7_3_Message.html>
// Also used as part "message/disposition-notification" of "multipart/report", which, however, will
// be handled separatedly.
// I've not seen any messages using this, so we do not attach these parts (maybe they're used to attach replies,
// which are unwanted at all).
// For now, we skip these parts at all; if desired, we could return DcMimeType::File/DC_MSG_File
// for selected and known subparts.
Viewtype::Unknown
}
mime::APPLICATION => Viewtype::File,
_ => Viewtype::Unknown,
@@ -1813,7 +1770,7 @@ mod tests {
use crate::{
chatlist::Chatlist,
config::Config,
constants::{Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
constants::Blocked,
message::{Message, MessageState, MessengerMessage},
receive_imf::receive_imf,
test_utils::TestContext,
@@ -3299,36 +3256,4 @@ Message.
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receive_eml() -> Result<()> {
let alice = TestContext::new_alice().await;
let mime_message = MimeMessage::from_bytes(
&alice,
include_bytes!("../test-data/message/attached-eml.eml"),
)
.await?;
assert_eq!(mime_message.parts.len(), 1);
assert_eq!(mime_message.parts[0].typ, Viewtype::File);
assert_eq!(
mime_message.parts[0].mimetype,
Some("message/rfc822".parse().unwrap(),)
);
assert_eq!(
mime_message.parts[0].msg,
"this is a classic email I attached the .EML file".to_string()
);
assert_eq!(
mime_message.parts[0].param.get(Param::File),
Some("$BLOBDIR/.eml")
);
assert_eq!(mime_message.parts[0].org_filename, Some(".eml".to_string()));
dbg!(mime_message);
Ok(())
}
}

View File

@@ -163,7 +163,7 @@ pub enum Param {
/// For Chats: timestamp of group name update.
GroupNameTimestamp = b'g',
/// For Chats: timestamp of member list update.
/// For Chats: timestamp of group name update.
MemberListTimestamp = b'k',
/// For Chats: timestamp of protection settings update.

View File

@@ -9,7 +9,6 @@ use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::{addr_cmp, Contact, Origin};
use crate::context::Context;
use crate::decrypt::DecryptionInfo;
use crate::events::EventType;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::message::Message;
@@ -166,7 +165,7 @@ impl Peerstate {
Self::from_stmt(context, query, paramsv![fp, fp, fp]).await
}
pub async fn from_verified_fingerprint_or_addr(
pub async fn from_nongossiped_fingerprint_or_addr(
context: &Context,
fingerprint: &Fingerprint,
addr: &str,
@@ -175,9 +174,9 @@ impl Peerstate {
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE verified_key_fingerprint=? \
WHERE public_key_fingerprint=? \
OR addr=? COLLATE NOCASE \
ORDER BY verified_key_fingerprint=? DESC, last_seen DESC LIMIT 1;";
ORDER BY public_key_fingerprint=? DESC, last_seen DESC LIMIT 1;";
let fp = fingerprint.hex();
Self::from_stmt(context, query, paramsv![fp, addr, fp]).await
}
@@ -517,22 +516,22 @@ impl Peerstate {
.with_context(|| format!("contact with peerstate.addr {:?} not found", &self.addr))?;
let chats = Chatlist::try_load(context, 0, None, Some(contact_id)).await?;
let msg = match &change {
PeerstateChange::FingerprintChange => {
stock_str::contact_setup_changed(context, self.addr.clone()).await
}
PeerstateChange::Aeap(new_addr) => {
let old_contact = Contact::load_from_db(context, contact_id).await?;
stock_str::aeap_addr_changed(
context,
old_contact.get_display_name(),
&self.addr,
new_addr,
)
.await
}
};
for (chat_id, msg_id) in chats.iter() {
let msg = match &change {
PeerstateChange::FingerprintChange => {
stock_str::contact_setup_changed(context, self.addr.clone()).await
}
PeerstateChange::Aeap(new_addr) => {
let old_contact = Contact::load_from_db(context, contact_id).await?;
stock_str::aeap_addr_changed(
context,
old_contact.get_display_name(),
&self.addr,
new_addr,
)
.await
}
};
let timestamp_sort = if let Some(msg_id) = msg_id {
let lastmsg = Message::load_from_db(context, *msg_id).await?;
lastmsg.timestamp_sort
@@ -546,23 +545,21 @@ impl Peerstate {
.await?
.unwrap_or(0)
};
chat::add_info_msg_with_cmd(
context,
*chat_id,
&msg,
SystemMessage::Unknown,
timestamp_sort,
Some(timestamp),
None,
None,
)
.await?;
if let PeerstateChange::Aeap(new_addr) = &change {
let chat = Chat::load_from_db(context, *chat_id).await?;
if chat.typ == Chattype::Group && !chat.is_protected() {
// Don't add an info_msg to the group, in order not to make the user think
// that the address was automatically replaced in the group.
continue;
}
// For security reasons, for now, we only do the AEAP transition if the fingerprint
// is verified (that's what from_verified_fingerprint_or_addr() does).
// In order to not have inconsistent group membership state, we then only do the
// transition in verified groups and in broadcast lists.
if (chat.typ == Chattype::Group && chat.is_protected())
|| chat.typ == Chattype::Broadcast
{
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
chat::remove_from_chat_contacts_table(context, *chat_id, contact_id).await?;
let (new_contact_id, _) =
@@ -575,18 +572,6 @@ impl Peerstate {
context.emit_event(EventType::ChatModified(*chat_id));
}
}
chat::add_info_msg_with_cmd(
context,
*chat_id,
&msg,
SystemMessage::Unknown,
timestamp_sort,
Some(timestamp),
None,
None,
)
.await?;
}
Ok(())
@@ -612,7 +597,7 @@ impl Peerstate {
/// In `drafts/aeap_mvp.md` there is a "big picture" overview over AEAP.
pub async fn maybe_do_aeap_transition(
context: &Context,
info: &mut DecryptionInfo,
info: &mut crate::e2ee::DecryptionInfo,
mime_parser: &crate::mimeparser::MimeMessage,
) -> Result<()> {
if let Some(peerstate) = &mut info.peerstate {
@@ -636,7 +621,7 @@ pub async fn maybe_do_aeap_transition(
&& mime_parser.from_is_signed
&& info.message_time > peerstate.last_seen
{
// Add info messages to chats with this (verified) contact
// Add an info messages to all chats with this contact
//
peerstate
.handle_setup_change(

View File

@@ -21,7 +21,7 @@ pub struct PlainText {
impl PlainText {
/// Convert plain text to HTML.
/// The function handles quotes, links, fixed and floating text paragraphs.
pub fn to_html(&self) -> String {
pub async fn to_html(&self) -> String {
static LINKIFY_MAIL_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r#"\b([\w.\-+]+@[\w.\-]+)\b"#).unwrap());
@@ -111,7 +111,8 @@ http://link-at-start-of-line.org
flowed: false,
delsp: false,
}
.to_html();
.to_html()
.await;
assert_eq!(
html,
r##"<!DOCTYPE html>
@@ -133,7 +134,8 @@ line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a
flowed: false,
delsp: false,
}
.to_html();
.to_html()
.await;
assert_eq!(
html,
r#"<!DOCTYPE html>
@@ -151,7 +153,8 @@ line with &lt;<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.l
flowed: false,
delsp: false,
}
.to_html();
.to_html()
.await;
assert_eq!(
html,
r#"<!DOCTYPE html>
@@ -169,7 +172,8 @@ line with nohttp://no.link here<br/>
flowed: false,
delsp: false,
}
.to_html();
.to_html()
.await;
assert_eq!(
html,
r#"<!DOCTYPE html>
@@ -187,7 +191,8 @@ just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:an
flowed: true,
delsp: false,
}
.to_html();
.to_html()
.await;
assert_eq!(
html,
r#"<!DOCTYPE html>
@@ -208,7 +213,8 @@ line still line<br/>
flowed: true,
delsp: true,
}
.to_html();
.to_html()
.await;
assert_eq!(
html,
r#"<!DOCTYPE html>
@@ -229,7 +235,8 @@ linestill line<br/>
flowed: false,
delsp: false,
}
.to_html();
.to_html()
.await;
assert_eq!(
html,
r#"<!DOCTYPE html>

View File

@@ -9,7 +9,7 @@ use anyhow::Result;
use chrono::{NaiveDateTime, NaiveTime};
use trust_dns_resolver::{config, AsyncResolver, TokioAsyncResolver};
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Status {
Ok = 1,
@@ -17,14 +17,14 @@ pub enum Status {
Broken = 3,
}
#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Protocol {
Smtp = 1,
Imap = 2,
}
#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Socket {
Automatic = 0,
@@ -39,21 +39,21 @@ impl Default for Socket {
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[derive(Debug, PartialEq, Clone)]
#[repr(u8)]
pub enum UsernamePattern {
Email = 1,
Emaillocalpart = 2,
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Oauth2Authorizer {
Yandex = 1,
Gmail = 2,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Server {
pub protocol: Protocol,
pub socket: Socket,
@@ -62,13 +62,13 @@ pub struct Server {
pub username_pattern: UsernamePattern,
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub struct ConfigDefault {
pub key: Config,
pub value: &'static str,
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub struct Provider {
/// Unique ID, corresponding to provider database filename.
pub id: &'static str,

View File

@@ -9,32 +9,20 @@ use std::collections::HashMap;
use once_cell::sync::Lazy;
// 163.md: 163.com
static P_163: Lazy<Provider> = Lazy::new(|| Provider {
static P_163: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "163",
status: Status::Ok,
before_login_hint: "",
status: Status::Broken,
before_login_hint: "163 Mail does not work since it forces the email clients to connect with an IMAP ID, which is currently not the case of Delta Chat.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/163",
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "imap.163.com",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "smtp.163.com",
port: 465,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
// aktivix.org.md: aktivix.org
@@ -1891,4 +1879,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 7, 5));
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 6, 4));

View File

@@ -2,9 +2,9 @@
# if the yaml import fails, run "pip install pyyaml"
import sys
import os
import yaml
import datetime
from pathlib import Path
out_all = ""
out_domains = ""
@@ -24,7 +24,7 @@ def cleanstr(s):
def file2id(f):
return f.stem
return os.path.basename(f).replace(".md", "")
def file2varname(f):
@@ -141,7 +141,7 @@ def process_data(data, file):
# finally, add the provider
global out_all, out_domains, out_ids
out_all += "// " + file.name + ": " + comment.strip(", ") + "\n"
out_all += "// " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
# also add provider with no special things to do -
# eg. _not_ supporting oauth2 is also an information and we can skip the mx-lookup in this case
@@ -151,7 +151,7 @@ def process_data(data, file):
def process_file(file):
print("processing file: {}".format(file), file=sys.stderr)
print("processing file: " + file, file=sys.stderr)
with open(file) as f:
# load_all() loads "---"-separated yamls -
# by coincidence, this is also the frontmatter separator :)
@@ -160,10 +160,11 @@ def process_file(file):
def process_dir(dir):
print("processing directory: {}".format(dir), file=sys.stderr)
files = sorted(f for f in dir.iterdir() if f.suffix == '.md')
print("processing directory: " + dir, file=sys.stderr)
files = [f for f in os.listdir(dir) if f.endswith(".md")]
files.sort()
for f in files:
process_file(f)
process_file(os.path.join(dir, f))
if __name__ == "__main__":
@@ -178,7 +179,7 @@ if __name__ == "__main__":
"use std::collections::HashMap;\n\n"
"use once_cell::sync::Lazy;\n\n")
process_dir(Path(sys.argv[1]))
process_dir(sys.argv[1])
out_all += "pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| [\n"
out_all += out_domains;

251
src/qr.rs
View File

@@ -1,9 +1,6 @@
//! # QR code module.
mod dclogin_scheme;
pub use dclogin_scheme::LoginOptions;
use anyhow::{anyhow, bail, ensure, Context as _, Error, Result};
use anyhow::{bail, ensure, Context as _, Error, Result};
use once_cell::sync::Lazy;
use percent_encoding::percent_decode_str;
use serde::Deserialize;
@@ -17,14 +14,11 @@ use crate::context::Context;
use crate::key::Fingerprint;
use crate::message::Message;
use crate::peerstate::Peerstate;
use crate::token;
use crate::tools::time;
use crate::{token, EventType};
use self::dclogin_scheme::configure_from_login_qr;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
const DCWEBRTC_SCHEME: &str = "DCWEBRTC:";
const MAILTO_SCHEME: &str = "mailto:";
const MATMSG_SCHEME: &str = "MATMSG:";
@@ -33,7 +27,7 @@ const SMTP_SCHEME: &str = "SMTP:";
const HTTP_SCHEME: &str = "http://";
const HTTPS_SCHEME: &str = "https://";
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub enum Qr {
AskVerifyContact {
contact_id: ContactId,
@@ -67,7 +61,6 @@ pub enum Qr {
},
Addr {
contact_id: ContactId,
draft: Option<String>,
},
Url {
url: String,
@@ -103,10 +96,6 @@ pub enum Qr {
invitenumber: String,
authcode: String,
},
Login {
address: String,
options: LoginOptions,
},
}
fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
@@ -125,8 +114,6 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
.context("failed to decode OPENPGP4FPR QR code")?
} else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) {
decode_account(qr)?
} else if starts_with_ignore_case(qr, DCLOGIN_SCHEME) {
dclogin_scheme::decode_login(qr)?
} else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) {
decode_webrtc_instance(context, qr)?
} else if qr.starts_with(MAILTO_SCHEME) {
@@ -356,14 +343,10 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
}
#[derive(Debug, Deserialize)]
struct CreateAccountSuccessResponse {
struct CreateAccountResponse {
email: String,
password: String,
}
#[derive(Debug, Deserialize)]
struct CreateAccountErrorResponse {
reason: String,
}
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
/// download additional information from the contained url and set the parameters.
@@ -371,42 +354,23 @@ struct CreateAccountErrorResponse {
#[allow(clippy::indexing_slicing)]
async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
let response = reqwest::Client::new().post(url_str).send().await?;
let response_status = response.status();
let response_text = response.text().await.with_context(|| {
format!(
"Cannot create account, request to {:?} failed: empty response",
url_str
)
})?;
if response_status.is_success() {
let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text)
.with_context(|| {
format!(
"Cannot create account, response from {:?} is malformed:\n{:?}",
url_str, response_text
)
})?;
context.set_config(Config::Addr, Some(&email)).await?;
context.set_config(Config::MailPw, Some(&password)).await?;
let parsed: CreateAccountResponse = reqwest::Client::new()
.post(url_str)
.send()
.await?
.json()
.await
.with_context(|| format!("Cannot create account, request to {:?} failed", url_str))?;
Ok(())
} else {
match serde_json::from_str::<CreateAccountErrorResponse>(&response_text) {
Ok(error) => Err(anyhow!(error.reason)),
Err(parse_error) => {
context.emit_event(EventType::Error(format!(
"Cannot create account, server response could not be parsed:\n{:#}\nraw response:\n{}",
parse_error, response_text
)));
bail!(
"Cannot create account, unexpected server response:\n{:?}",
response_text
)
}
}
}
context
.set_config(Config::Addr, Some(&parsed.email))
.await?;
context
.set_config(Config::MailPw, Some(&parsed.password))
.await?;
Ok(())
}
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
@@ -474,9 +438,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
context.sync_qr_code_tokens(chat_id).await?;
context.send_sync_msg().await?;
}
Qr::Login { address, options } => {
configure_from_login_qr(context, &address, options).await?
}
_ => bail!("qr code {:?} does not contain config", qr),
}
@@ -490,52 +451,15 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
let payload = &qr[MAILTO_SCHEME.len()..];
let (addr, query) = if let Some(query_index) = payload.find('?') {
(&payload[..query_index], &payload[query_index + 1..])
let addr = if let Some(query_index) = payload.find('?') {
&payload[..query_index]
} else {
(payload, "")
};
let param: BTreeMap<&str, &str> = query
.split('&')
.filter_map(|s| {
if let [key, value] = s.splitn(2, '=').collect::<Vec<_>>()[..] {
Some((key, value))
} else {
None
}
})
.collect();
let subject = if let Some(subject) = param.get("subject") {
subject.to_string()
} else {
"".to_string()
};
let draft = if let Some(body) = param.get("body") {
if subject.is_empty() {
body.to_string()
} else {
subject + "\n" + body
}
} else {
subject
};
let draft = draft.replace('+', "%20"); // sometimes spaces are encoded as `+`
let draft = match percent_decode_str(&draft).decode_utf8() {
Ok(decoded_draft) => decoded_draft.to_string(),
Err(_err) => draft,
payload
};
let addr = normalize_address(addr)?;
let name = "".to_string();
Qr::from_address(
context,
name,
addr,
if draft.is_empty() { None } else { Some(draft) },
)
.await
Qr::from_address(context, name, addr).await
}
/// Extract address for the smtp scheme.
@@ -553,7 +477,7 @@ async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
let addr = normalize_address(addr)?;
let name = "".to_string();
Qr::from_address(context, name, addr, None).await
Qr::from_address(context, name, addr).await
}
/// Extract address for the matmsg scheme.
@@ -578,7 +502,7 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
let addr = normalize_address(addr)?;
let name = "".to_string();
Qr::from_address(context, name, addr, None).await
Qr::from_address(context, name, addr).await
}
static VCARD_NAME_RE: Lazy<regex::Regex> =
@@ -607,19 +531,14 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
bail!("Bad e-mail address");
};
Qr::from_address(context, name, addr, None).await
Qr::from_address(context, name, addr).await
}
impl Qr {
pub async fn from_address(
context: &Context,
name: String,
addr: String,
draft: Option<String>,
) -> Result<Self> {
pub async fn from_address(context: &Context, name: String, addr: String) -> Result<Self> {
let (contact_id, _) =
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan).await?;
Ok(Qr::Addr { contact_id, draft })
Ok(Qr::Addr { contact_id })
}
}
@@ -700,13 +619,12 @@ mod tests {
"BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD"
).await?;
if let Qr::Addr { contact_id, draft } = qr {
if let Qr::Addr { contact_id } = qr {
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "stress@test.local");
assert_eq!(contact.get_name(), "First Last");
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_display_name(), "First Last");
assert!(draft.is_none());
} else {
bail!("Wrong QR code type");
}
@@ -724,10 +642,9 @@ mod tests {
)
.await?;
if let Qr::Addr { contact_id, draft } = qr {
if let Qr::Addr { contact_id } = qr {
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "stress@test.local");
assert!(draft.is_none());
} else {
bail!("Wrong QR code type");
}
@@ -741,22 +658,20 @@ mod tests {
let qr = check_qr(
&ctx.ctx,
"mailto:stress@test.local?subject=hello&body=beautiful+world",
"mailto:stress@test.local?subject=hello&body=world",
)
.await?;
if let Qr::Addr { contact_id, draft } = qr {
if let Qr::Addr { contact_id } = qr {
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "stress@test.local");
assert_eq!(draft.unwrap(), "hello\nbeautiful world");
} else {
bail!("Wrong QR code type");
}
let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org").await?;
if let Qr::Addr { contact_id, draft } = res {
if let Qr::Addr { contact_id } = res {
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "no-questionmark@example.org");
assert!(draft.is_none());
} else {
bail!("Wrong QR code type");
}
@@ -771,12 +686,11 @@ mod tests {
async fn test_decode_smtp() -> Result<()> {
let ctx = TestContext::new().await;
if let Qr::Addr { contact_id, draft } =
if let Qr::Addr { contact_id } =
check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await?
{
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
assert_eq!(contact.get_addr(), "stress@test.local");
assert!(draft.is_none());
} else {
bail!("Wrong QR code type");
}
@@ -1039,103 +953,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_and_apply_dclogin() -> Result<()> {
let ctx = TestContext::new().await;
let result = check_qr(&ctx.ctx, "dclogin:usename+extension@host?p=1234&v=1").await?;
if let Qr::Login { address, options } = result {
assert_eq!(address, "usename+extension@host".to_owned());
if let LoginOptions::V1 { mail_pw, .. } = options {
assert_eq!(mail_pw, "1234".to_owned());
} else {
bail!("wrong type")
}
} else {
bail!("wrong type")
}
assert!(ctx.ctx.get_config(Config::Addr).await?.is_none());
assert!(ctx.ctx.get_config(Config::MailPw).await?.is_none());
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&v=1").await?;
assert_eq!(
ctx.ctx.get_config(Config::Addr).await?,
Some("username+extension@host".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::MailPw).await?,
Some("1234".to_owned())
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_and_apply_dclogin_advanced_options() -> Result<()> {
let ctx = TestContext::new().await;
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&spw=4321&sh=send.host&sp=7273&su=SendUser&ih=host.tld&ip=4343&iu=user&ipw=password&is=ssl&ic=1&sc=3&ss=plain&v=1").await?;
assert_eq!(
ctx.ctx.get_config(Config::Addr).await?,
Some("username+extension@host".to_owned())
);
// `p=1234` is ignored, because `ipw=password` is set
assert_eq!(
ctx.ctx.get_config(Config::MailServer).await?,
Some("host.tld".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::MailPort).await?,
Some("4343".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::MailUser).await?,
Some("user".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::MailPw).await?,
Some("password".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::MailSecurity).await?,
Some("1".to_owned()) // ssl
);
assert_eq!(
ctx.ctx.get_config(Config::ImapCertificateChecks).await?,
Some("1".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::SendPw).await?,
Some("4321".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::SendServer).await?,
Some("send.host".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::SendPort).await?,
Some("7273".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::SendUser).await?,
Some("SendUser".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::SmtpCertificateChecks).await?,
Some("3".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::SendSecurity).await?,
Some("3".to_owned()) // plain
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_account() -> Result<()> {
let ctx = TestContext::new().await;

View File

@@ -1,388 +0,0 @@
use std::collections::HashMap;
use crate::config::Config;
use crate::context::Context;
use crate::provider::Socket;
use crate::{contact, login_param::CertificateChecks};
use anyhow::{bail, Context as _, Result};
use num_traits::cast::ToPrimitive;
use super::{Qr, DCLOGIN_SCHEME};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoginOptions {
UnsuportedVersion(u32),
V1 {
mail_pw: String,
imap_host: Option<String>,
imap_port: Option<u16>,
imap_username: Option<String>,
imap_password: Option<String>,
imap_security: Option<Socket>,
imap_certificate_checks: Option<CertificateChecks>,
smtp_host: Option<String>,
smtp_port: Option<u16>,
smtp_username: Option<String>,
smtp_password: Option<String>,
smtp_security: Option<Socket>,
smtp_certificate_checks: Option<CertificateChecks>,
},
}
/// scheme: `dclogin://user@host/?p=password&v=1[&options]`
/// read more about the scheme at <https://github.com/deltachat/interface/blob/master/uri-schemes.md#DCLOGIN>
pub(super) fn decode_login(qr: &str) -> Result<Qr> {
let url = url::Url::parse(qr).with_context(|| format!("Malformed url: {:?}", qr))?;
let url_without_scheme = qr
.get(DCLOGIN_SCHEME.len()..)
.context("invalid DCLOGIN payload E1")?;
let payload = url_without_scheme
.strip_prefix("//")
.unwrap_or(url_without_scheme);
let addr = payload
.split(|c| c == '?' || c == '/')
.next()
.context("invalid DCLOGIN payload E3")?;
if url.scheme().eq_ignore_ascii_case("dclogin") {
let options = url.query_pairs();
if options.count() == 0 {
bail!("invalid DCLOGIN payload E4")
}
// load options into hashmap
let parameter_map: HashMap<String, String> = options
.map(|(key, value)| (key.into_owned(), value.into_owned()))
.collect();
// check if username is there
if !contact::may_be_valid_addr(addr) {
bail!("invalid DCLOGIN payload: invalid username E5");
}
// apply to result struct
let options: LoginOptions = match parameter_map.get("v").map(|i| i.parse::<u32>()) {
Some(Ok(1)) => LoginOptions::V1 {
mail_pw: parameter_map
.get("p")
.map(|s| s.to_owned())
.context("password missing")?,
imap_host: parameter_map.get("ih").map(|s| s.to_owned()),
imap_port: parse_port(parameter_map.get("ip"))
.context("could not parse imap port")?,
imap_username: parameter_map.get("iu").map(|s| s.to_owned()),
imap_password: parameter_map.get("ipw").map(|s| s.to_owned()),
imap_security: parse_socket_security(parameter_map.get("is"))?,
imap_certificate_checks: parse_certificate_checks(parameter_map.get("ic"))?,
smtp_host: parameter_map.get("sh").map(|s| s.to_owned()),
smtp_port: parse_port(parameter_map.get("sp"))
.context("could not parse smtp port")?,
smtp_username: parameter_map.get("su").map(|s| s.to_owned()),
smtp_password: parameter_map.get("spw").map(|s| s.to_owned()),
smtp_security: parse_socket_security(parameter_map.get("ss"))?,
smtp_certificate_checks: parse_certificate_checks(parameter_map.get("sc"))?,
},
Some(Ok(v)) => LoginOptions::UnsuportedVersion(v),
Some(Err(_)) => bail!("version could not be parsed as number E6"),
None => bail!("invalid DCLOGIN payload: version missing E7"),
};
Ok(Qr::Login {
address: addr.to_owned(),
options,
})
} else {
bail!("Bad scheme for account URL: {:?}.", payload);
}
}
fn parse_port(port: Option<&String>) -> core::result::Result<Option<u16>, std::num::ParseIntError> {
match port {
Some(p) => Ok(Some(p.parse::<u16>()?)),
None => Ok(None),
}
}
fn parse_socket_security(security: Option<&String>) -> Result<Option<Socket>> {
Ok(match security.map(|s| s.as_str()) {
Some("ssl") => Some(Socket::Ssl),
Some("starttls") => Some(Socket::Starttls),
Some("default") => Some(Socket::Automatic),
Some("plain") => Some(Socket::Plain),
Some(other) => bail!("Unknown security level: {}", other),
None => None,
})
}
fn parse_certificate_checks(
certificate_checks: Option<&String>,
) -> Result<Option<CertificateChecks>> {
Ok(match certificate_checks.map(|s| s.as_str()) {
Some("0") => Some(CertificateChecks::Automatic),
Some("1") => Some(CertificateChecks::Strict),
Some("3") => Some(CertificateChecks::AcceptInvalidCertificates),
Some(other) => bail!("Unknown certificatecheck level: {}", other),
None => None,
})
}
pub(crate) async fn configure_from_login_qr(
context: &Context,
address: &str,
options: LoginOptions,
) -> Result<()> {
context.set_config(Config::Addr, Some(address)).await?;
match options {
LoginOptions::V1 {
mail_pw,
imap_host,
imap_port,
imap_username,
imap_password,
imap_security,
imap_certificate_checks,
smtp_host,
smtp_port,
smtp_username,
smtp_password,
smtp_security,
smtp_certificate_checks,
} => {
context.set_config(Config::MailPw, Some(&mail_pw)).await?;
if let Some(value) = imap_host {
context.set_config(Config::MailServer, Some(&value)).await?;
}
if let Some(value) = imap_port {
context
.set_config(Config::MailPort, Some(&value.to_string()))
.await?;
}
if let Some(value) = imap_username {
context.set_config(Config::MailUser, Some(&value)).await?;
}
if let Some(value) = imap_password {
context.set_config(Config::MailPw, Some(&value)).await?;
}
if let Some(value) = imap_security {
let code = value
.to_u8()
.context("could not convert imap security value to number")?;
context
.set_config(Config::MailSecurity, Some(&code.to_string()))
.await?;
}
if let Some(value) = imap_certificate_checks {
let code = value
.to_u32()
.context("could not convert imap certificate checks value to number")?;
context
.set_config(Config::ImapCertificateChecks, Some(&code.to_string()))
.await?;
}
if let Some(value) = smtp_host {
context.set_config(Config::SendServer, Some(&value)).await?;
}
if let Some(value) = smtp_port {
context
.set_config(Config::SendPort, Some(&value.to_string()))
.await?;
}
if let Some(value) = smtp_username {
context.set_config(Config::SendUser, Some(&value)).await?;
}
if let Some(value) = smtp_password {
context.set_config(Config::SendPw, Some(&value)).await?;
}
if let Some(value) = smtp_security {
let code = value
.to_u8()
.context("could not convert smtp security value to number")?;
context
.set_config(Config::SendSecurity, Some(&code.to_string()))
.await?;
}
if let Some(value) = smtp_certificate_checks {
let code = value
.to_u32()
.context("could not convert smtp certificate checks value to number")?;
context
.set_config(Config::SmtpCertificateChecks, Some(&code.to_string()))
.await?;
}
Ok(())
}
_ => bail!(
"DeltaChat does not understand this QR Code yet, please update the app and try again."
),
}
}
#[cfg(test)]
mod test {
use super::{decode_login, LoginOptions};
use crate::{login_param::CertificateChecks, provider::Socket, qr::Qr};
use anyhow::{self, bail};
macro_rules! login_options_just_pw {
($pw: expr) => {
LoginOptions::V1 {
mail_pw: $pw,
imap_host: None,
imap_port: None,
imap_username: None,
imap_password: None,
imap_security: None,
imap_certificate_checks: None,
smtp_host: None,
smtp_port: None,
smtp_username: None,
smtp_password: None,
smtp_security: None,
smtp_certificate_checks: None,
}
};
}
#[test]
fn minimal_no_options() -> anyhow::Result<()> {
let result = decode_login("dclogin://email@host.tld?p=123&v=1")?;
if let Qr::Login { address, options } = result {
assert_eq!(address, "email@host.tld".to_owned());
assert_eq!(options, login_options_just_pw!("123".to_owned()));
} else {
bail!("wrong type")
}
let result = decode_login("dclogin://email@host.tld/?p=123456&v=1")?;
if let Qr::Login { address, options } = result {
assert_eq!(address, "email@host.tld".to_owned());
assert_eq!(options, login_options_just_pw!("123456".to_owned()));
} else {
bail!("wrong type")
}
let result = decode_login("dclogin://email@host.tld/ignored/path?p=123456&v=1")?;
if let Qr::Login { address, options } = result {
assert_eq!(address, "email@host.tld".to_owned());
assert_eq!(options, login_options_just_pw!("123456".to_owned()));
} else {
bail!("wrong type")
}
Ok(())
}
#[test]
fn minimal_no_options_no_double_slash() -> anyhow::Result<()> {
let result = decode_login("dclogin:email@host.tld?p=123&v=1")?;
if let Qr::Login { address, options } = result {
assert_eq!(address, "email@host.tld".to_owned());
assert_eq!(options, login_options_just_pw!("123".to_owned()));
} else {
bail!("wrong type")
}
let result = decode_login("dclogin:email@host.tld/?p=123456&v=1")?;
if let Qr::Login { address, options } = result {
assert_eq!(address, "email@host.tld".to_owned());
assert_eq!(options, login_options_just_pw!("123456".to_owned()));
} else {
bail!("wrong type")
}
let result = decode_login("dclogin:email@host.tld/ignored/path?p=123456&v=1")?;
if let Qr::Login { address, options } = result {
assert_eq!(address, "email@host.tld".to_owned());
assert_eq!(options, login_options_just_pw!("123456".to_owned()));
} else {
bail!("wrong type")
}
Ok(())
}
#[test]
fn no_version_set() {
assert!(decode_login("dclogin:email@host.tld?p=123").is_err());
}
#[test]
fn invalid_version_set() {
assert!(decode_login("dclogin:email@host.tld?p=123&v=").is_err());
assert!(decode_login("dclogin:email@host.tld?p=123&v=%40").is_err());
assert!(decode_login("dclogin:email@host.tld?p=123&v=-20").is_err());
assert!(decode_login("dclogin:email@host.tld?p=123&v=hi").is_err());
}
#[test]
fn version_too_new() -> anyhow::Result<()> {
let result = decode_login("dclogin:email@host.tld/?p=123456&v=2")?;
if let Qr::Login { options, .. } = result {
assert_eq!(options, LoginOptions::UnsuportedVersion(2));
} else {
bail!("wrong type");
}
let result = decode_login("dclogin:email@host.tld/?p=123456&v=5")?;
if let Qr::Login { options, .. } = result {
assert_eq!(options, LoginOptions::UnsuportedVersion(5));
} else {
bail!("wrong type");
}
Ok(())
}
#[test]
fn all_advanced_options() -> anyhow::Result<()> {
let result = decode_login(
"dclogin:email@host.tld?p=secret&v=1&ih=imap.host.tld&ip=4000&iu=max&ipw=87654&is=ssl&ic=1&sh=mail.host.tld&sp=3000&su=max@host.tld&spw=3242HS&ss=plain&sc=3",
)?;
if let Qr::Login { address, options } = result {
assert_eq!(address, "email@host.tld".to_owned());
assert_eq!(
options,
LoginOptions::V1 {
mail_pw: "secret".to_owned(),
imap_host: Some("imap.host.tld".to_owned()),
imap_port: Some(4000),
imap_username: Some("max".to_owned()),
imap_password: Some("87654".to_owned()),
imap_security: Some(Socket::Ssl),
imap_certificate_checks: Some(CertificateChecks::Strict),
smtp_host: Some("mail.host.tld".to_owned()),
smtp_port: Some(3000),
smtp_username: Some("max@host.tld".to_owned()),
smtp_password: Some("3242HS".to_owned()),
smtp_security: Some(Socket::Plain),
smtp_certificate_checks: Some(CertificateChecks::AcceptInvalidCertificates),
}
);
} else {
bail!("wrong type")
}
Ok(())
}
#[test]
fn uri_encoded_password() -> anyhow::Result<()> {
let result = decode_login(
"dclogin:email@host.tld?p=%7BDaehFl%3B%22as%40%21fhdodn5%24234%22%7B%7Dfg&v=1",
)?;
if let Qr::Login { address, options } = result {
assert_eq!(address, "email@host.tld".to_owned());
assert_eq!(
options,
login_options_just_pw!("{DaehFl;\"as@!fhdodn5$234\"{}fg".to_owned())
);
} else {
bail!("wrong type")
}
Ok(())
}
#[test]
fn email_with_plus_extension() -> anyhow::Result<()> {
let result = decode_login("dclogin:usename+extension@host?p=1234&v=1")?;
if let Qr::Login { address, options } = result {
assert_eq!(address, "usename+extension@host".to_owned());
assert_eq!(options, login_options_just_pw!("1234".to_owned()));
} else {
bail!("wrong type")
}
Ok(())
}
}

View File

@@ -51,7 +51,7 @@ impl Ratelimit {
/// Returns true if it is allowed to send a message.
fn can_send_at(&self, now: SystemTime) -> bool {
self.current_value_at(now) + 1.0 <= self.quota
self.current_value_at(now) <= self.quota
}
/// Returns true if can send another message now.
@@ -62,7 +62,7 @@ impl Ratelimit {
}
fn send_at(&mut self, now: SystemTime) {
self.current_value = f64::min(self.quota, self.current_value_at(now) + 1.0);
self.current_value = self.current_value_at(now) + 1.0;
self.last_update = now;
}
@@ -77,10 +77,10 @@ impl Ratelimit {
fn until_can_send_at(&self, now: SystemTime) -> Duration {
let current_value = self.current_value_at(now);
if current_value + 1.0 <= self.quota {
if current_value <= self.quota {
Duration::ZERO
} else {
let requirement = current_value + 1.0 - self.quota;
let requirement = current_value - self.quota;
let rate = self.quota / self.window.as_secs_f64();
Duration::from_secs_f64(requirement / rate)
}
@@ -109,6 +109,8 @@ mod tests {
ratelimit.send_at(now);
assert!(ratelimit.can_send_at(now));
ratelimit.send_at(now);
assert!(ratelimit.can_send_at(now));
ratelimit.send_at(now);
// Can't send more messages now.
assert!(!ratelimit.can_send_at(now));
@@ -123,8 +125,11 @@ mod tests {
// Send one more message anyway, over quota.
ratelimit.send_at(now);
// Always can send another message after 20 seconds,
// leaky bucket never overflows.
// Waiting 20 seconds is not enough.
let now = now + Duration::from_secs(20);
assert!(!ratelimit.can_send_at(now));
// Can send another message after 40 seconds.
let now = now + Duration::from_secs(20);
assert!(ratelimit.can_send_at(now));

View File

@@ -130,14 +130,15 @@ pub(crate) async fn receive_imf_inner(
context,
"Message already partly in DB, replacing by full message."
);
Some(old_msg_id)
old_msg_id.delete_from_db(context).await?;
true
} else {
// the message was probably moved around.
info!(context, "Message already in DB, doing nothing.");
return Ok(None);
}
} else {
None
false
};
// the function returns the number of created messages in the database
@@ -188,9 +189,8 @@ pub(crate) async fn receive_imf_inner(
sent_timestamp,
rcvd_timestamp,
from_id,
seen || replace_partial_download.is_some(),
seen || replace_partial_download,
is_partial_download,
replace_partial_download,
fetching_existing_messages,
prevent_rename,
)
@@ -322,7 +322,7 @@ pub(crate) async fn receive_imf_inner(
}
}
if replace_partial_download.is_some() {
if replace_partial_download {
context.emit_msgs_changed(chat_id, MsgId::new(0));
} else if !chat_id.is_trash() {
let fresh = received_msg.state == MessageState::InFresh;
@@ -388,9 +388,6 @@ pub async fn from_field_to_contact_id(
}
}
/// Creates a `ReceivedMsg` from given parts which might consist of
/// mulitple messages (if there are multiple attachments).
/// Every entry in `mime_parser.parts` produces a new row in the `msgs` table.
#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
async fn add_parts(
context: &Context,
@@ -404,7 +401,6 @@ async fn add_parts(
from_id: ContactId,
seen: bool,
is_partial_download: Option<u32>,
replace_msg_id: Option<MsgId>,
fetching_existing_messages: bool,
prevent_rename: bool,
) -> Result<ReceivedMsg> {
@@ -495,14 +491,14 @@ async fn add_parts(
}
let test_normal_chat = if from_id == ContactId::UNDEFINED {
None
Default::default()
} else {
ChatIdBlocked::lookup_by_contact(context, from_id).await?
};
if chat_id.is_none() && mime_parser.delivery_report.is_some() {
if chat_id.is_none() && mime_parser.failure_report.is_some() {
chat_id = Some(DC_CHAT_ID_TRASH);
info!(context, "Message is a DSN (TRASH)",);
info!(context, "Message belongs to an NDN (TRASH)",);
}
if chat_id.is_none() {
@@ -516,9 +512,6 @@ async fn add_parts(
}
}
// signals wether the current user is a bot
let is_bot = context.get_config(Config::Bot).await?.is_some();
if chat_id.is_none() {
// try to create a group
@@ -527,7 +520,6 @@ async fn add_parts(
id: _,
blocked: Blocked::Not,
}) => Blocked::Not,
_ if is_bot => Blocked::Not,
_ => Blocked::Request,
};
@@ -547,9 +539,6 @@ async fn add_parts(
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
// if the chat is somehow blocked but we want to create a non-blocked chat,
// unblock the chat
if chat_id_blocked != Blocked::Not && create_blocked == Blocked::Not {
new_chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
@@ -647,10 +636,10 @@ async fn add_parts(
Blocked::Not
} else {
let contact = Contact::load_from_db(context, from_id).await?;
match contact.is_blocked() {
true => Blocked::Yes,
false if is_bot => Blocked::Not,
false => Blocked::Request,
if contact.is_blocked() {
Blocked::Yes
} else {
Blocked::Request
}
};
@@ -686,17 +675,12 @@ async fn add_parts(
}
}
state = if seen
|| fetching_existing_messages
|| is_mdn
|| location_kml_is
|| securejoin_seen
|| chat_id_blocked == Blocked::Yes
{
MessageState::InSeen
} else {
MessageState::InFresh
};
state =
if seen || fetching_existing_messages || is_mdn || location_kml_is || securejoin_seen {
MessageState::InSeen
} else {
MessageState::InFresh
};
} else {
// Outgoing
@@ -1161,17 +1145,6 @@ INSERT INTO msgs
}
drop(conn);
if let Some(replace_msg_id) = replace_msg_id {
if let Some(created_msg_id) = created_db_entries.pop() {
context
.merge_messages(created_msg_id, replace_msg_id)
.await?;
created_db_entries.push(replace_msg_id);
} else {
replace_msg_id.delete_from_db(context).await?;
}
}
chat_id.unarchive_if_not_muted(context).await?;
info!(
@@ -2674,7 +2647,7 @@ mod tests {
"shenauithz@testrun.org",
"Mr.un2NYERi1RM.lbQ5F9q-QyJ@tiscali.it",
include_bytes!("../test-data/message/tiscali_ndn.eml"),
Some("Delivery status notification This is an automatically generated Delivery Status Notification. \n\nDelivery to the following recipients was aborted after 2 second(s):\n\n * shenauithz@testrun.org"),
Some("Delivery to at least one recipient failed."),
)
.await;
}
@@ -2751,32 +2724,6 @@ mod tests {
.await;
}
/// Tests that text part is not squashed into OpenPGP attachment.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_ndn_with_attachment() {
test_parse_ndn(
"alice@example.org",
"bob@example.net",
"Mr.I6Da6dXcTel.TroC5J3uSDH@example.org",
include_bytes!("../test-data/message/ndn_with_attachment.eml"),
Some("Undelivered Mail Returned to Sender This is the mail system at host relay01.example.org.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n<bob@example.net>: host mx2.example.net[80.241.60.215] said: 552 5.2.2\n <bob@example.net>: Recipient address rejected: Mailbox quota exceeded (in\n reply to RCPT TO command)\n\n<bob2@example.net>: host mx1.example.net[80.241.60.212] said: 552 5.2.2\n <bob2@example.net>: Recipient address rejected: Mailbox quota\n exceeded (in reply to RCPT TO command)")
)
.await;
}
/// Test that DSN is not treated as NDN if Action: is not "failed"
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_dsn_relayed() {
test_parse_ndn(
"anon_1@posteo.de",
"anon_2@gmx.at",
"8b7b1a9d0c8cc588c7bcac47f5687634@posteo.de",
include_bytes!("../test-data/message/dsn_relayed.eml"),
None,
)
.await;
}
// ndn = Non Delivery Notification
async fn test_parse_ndn(
self_addr: &str,
@@ -2826,14 +2773,7 @@ mod tests {
receive_imf(&t, raw_ndn, false).await.unwrap();
let msg = Message::load_from_db(&t, msg_id).await.unwrap();
assert_eq!(
msg.state,
if error_msg.is_some() {
MessageState::OutFailed
} else {
MessageState::OutDelivered
}
);
assert_eq!(msg.state, MessageState::OutFailed);
assert_eq!(msg.error(), error_msg.map(|error| error.to_string()));
}
@@ -2947,10 +2887,6 @@ mod tests {
assert!(chat.is_mailing_list());
assert!(chat.can_send(&t.ctx).await?);
assert_eq!(
chat.get_mailinglist_addr(),
"reply+elernshsetushoyseshetihseusaferuhsedtisneu@reply.github.com"
);
assert_eq!(chat.name, "deltachat/deltachat-core-rust");
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await?.len(), 1);
@@ -2958,7 +2894,6 @@ mod tests {
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?;
assert!(!chat.can_send(&t.ctx).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?;
assert_eq!(chats.len(), 1);
@@ -3017,7 +2952,6 @@ mod tests {
let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
assert_eq!(chat.name, "delta-dev");
assert!(chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "delta@codespeak.net");
let msg = get_chat_msg(&t, chat_id, 0, 1).await;
let contact1 = Contact::load_from_db(&t.ctx, msg.from_id).await.unwrap();
@@ -3121,14 +3055,8 @@ Hello mailinglist!\r\n"
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); // Test that the message disappeared
t.evtracker.consume_events();
receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap();
// Check that no notification is displayed for blocked mailing list message.
while let Ok(event) = t.evtracker.try_recv() {
assert!(!matches!(event.typ, EventType::IncomingMsg { .. }));
}
// Test that the mailing list stays disappeared
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); // Test that the message is not shown
@@ -3252,7 +3180,7 @@ Hello mailinglist!\r\n"
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_majordomo_mailing_list() -> Result<()> {
async fn test_majordomo_mailing_list() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3279,8 +3207,6 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "mylist@bar.org");
assert_eq!(chat.name, "ola");
assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1);
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
// receive another message with no sender name but the same address,
// make sure this lands in the same chat
@@ -3300,12 +3226,10 @@ Hello mailinglist!\r\n"
.await
.unwrap();
assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 2);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mailchimp_mailing_list() -> Result<()> {
async fn test_mailchimp_mailing_list() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3332,14 +3256,10 @@ Hello mailinglist!\r\n"
"399fc0402f1b154b67965632e.100761.list-id.mcsv.net"
);
assert_eq!(chat.name, "Atlas Obscura");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dhl_mailing_list() -> Result<()> {
async fn test_dhl_mailing_list() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3361,14 +3281,10 @@ Hello mailinglist!\r\n"
assert_eq!(chat.blocked, Blocked::Request);
assert_eq!(chat.grpid, "1234ABCD-123LMNO.mailing.dhl.de");
assert_eq!(chat.name, "DHL Paket");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dpd_mailing_list() -> Result<()> {
async fn test_dpd_mailing_list() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3390,10 +3306,6 @@ Hello mailinglist!\r\n"
assert_eq!(chat.blocked, Blocked::Request);
assert_eq!(chat.grpid, "dpdde.mxmail.service.dpd.de");
assert_eq!(chat.name, "DPD");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -3411,8 +3323,6 @@ Hello mailinglist!\r\n"
assert_eq!(chat.typ, Chattype::Mailinglist);
assert_eq!(chat.grpid, "96540.xt.local");
assert_eq!(chat.name, "Microsoft Store");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
receive_imf(
&t,
@@ -3424,8 +3334,6 @@ Hello mailinglist!\r\n"
assert_eq!(chat.typ, Chattype::Mailinglist);
assert_eq!(chat.grpid, "121231234.xt.local");
assert_eq!(chat.name, "DER SPIEGEL Kundenservice");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
Ok(())
}
@@ -3447,8 +3355,6 @@ Hello mailinglist!\r\n"
assert_eq!(chat.typ, Chattype::Mailinglist);
assert_eq!(chat.grpid, "51231231231231231231231232869f58.xing.com");
assert_eq!(chat.name, "xing.com");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
Ok(())
}
@@ -3572,27 +3478,6 @@ Hello mailinglist!\r\n"
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mailing_list_chat_message() {
let t = TestContext::new_alice().await;
receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_chat_message.eml"),
false,
)
.await
.unwrap();
let msg = t.get_last_msg().await;
assert_eq!(msg.text, Some("hello, this is a test 👋\n\n_______________________________________________\nTest1 mailing list -- test1@example.net\nTo unsubscribe send an email to test1-leave@example.net".to_string()));
assert!(!msg.has_html());
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
assert_eq!(chat.typ, Chattype::Mailinglist);
assert_eq!(chat.blocked, Blocked::Request);
assert_eq!(chat.grpid, "test1.example.net");
assert_eq!(chat.name, "Test1");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_show_tokens_in_contacts_list() {
check_dont_show_in_contacts_list(
@@ -4949,7 +4834,7 @@ Reply from different address
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_long_filenames() -> Result<()> {
let mut tcm = TestContextManager::new();
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -5001,7 +4886,7 @@ Reply from different address
/// Tests that contact request is accepted automatically on outgoing message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_accept_outgoing() -> Result<()> {
let mut tcm = TestContextManager::new();
let mut tcm = TestContextManager::new().await;
let alice1 = tcm.alice().await;
let alice2 = tcm.alice().await;
let bob1 = tcm.bob().await;
@@ -5046,7 +4931,7 @@ Reply from different address
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing_private_reply_multidevice() -> Result<()> {
let mut tcm = TestContextManager::new();
let mut tcm = TestContextManager::new().await;
let alice1 = tcm.alice().await;
let alice2 = tcm.alice().await;
let bob = tcm.bob().await;
@@ -5121,20 +5006,9 @@ Reply from different address
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_auto_accept_for_bots() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::Bot, Some("1")).await.unwrap();
receive_imf(&t, MSGRMSG, false).await?;
let msg = t.get_last_msg().await;
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
assert!(!chat.is_contact_request());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_private_reply_to_blocked_account() -> Result<()> {
let mut tcm = TestContextManager::new();
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;

View File

@@ -1,6 +1,6 @@
use anyhow::{bail, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use futures::try_join;
use futures::{join, try_join};
use futures_lite::FutureExt;
use tokio::task;
@@ -42,7 +42,7 @@ impl Context {
pub async fn maybe_network(&self) {
let lock = self.scheduler.read().await;
if let Some(scheduler) = &*lock {
scheduler.maybe_network();
scheduler.maybe_network().await;
}
connectivity::idle_interrupted(lock).await;
}
@@ -51,32 +51,32 @@ impl Context {
pub async fn maybe_network_lost(&self) {
let lock = self.scheduler.read().await;
if let Some(scheduler) = &*lock {
scheduler.maybe_network_lost();
scheduler.maybe_network_lost().await;
}
connectivity::maybe_network_lost(self, lock).await;
}
pub(crate) async fn interrupt_inbox(&self, info: InterruptInfo) {
if let Some(scheduler) = &*self.scheduler.read().await {
scheduler.interrupt_inbox(info);
scheduler.interrupt_inbox(info).await;
}
}
pub(crate) async fn interrupt_smtp(&self, info: InterruptInfo) {
if let Some(scheduler) = &*self.scheduler.read().await {
scheduler.interrupt_smtp(info);
scheduler.interrupt_smtp(info).await;
}
}
pub(crate) async fn interrupt_ephemeral_task(&self) {
if let Some(scheduler) = &*self.scheduler.read().await {
scheduler.interrupt_ephemeral_task();
scheduler.interrupt_ephemeral_task().await;
}
}
pub(crate) async fn interrupt_location(&self) {
if let Some(scheduler) = &*self.scheduler.read().await {
scheduler.interrupt_location();
scheduler.interrupt_location().await;
}
}
}
@@ -324,25 +324,29 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
let mut timeout = None;
loop {
if let Err(err) = send_smtp_messages(&ctx, &mut connection).await {
warn!(ctx, "send_smtp_messages failed: {:#}", err);
timeout = Some(timeout.map_or(30, |timeout: u64| timeout.saturating_mul(3)))
} else {
let duration_until_can_send = ctx.ratelimit.read().await.until_can_send();
if !duration_until_can_send.is_zero() {
info!(
ctx,
"smtp got rate limited, waiting for {} until can send again",
duration_to_str(duration_until_can_send)
);
tokio::time::timeout(duration_until_can_send, async {
idle_interrupt_receiver.recv().await.unwrap_or_default()
})
.await
.unwrap_or_default();
continue;
match send_smtp_messages(&ctx, &mut connection).await {
Err(err) => {
warn!(ctx, "send_smtp_messages failed: {:#}", err);
timeout = Some(timeout.map_or(30, |timeout: u64| timeout.saturating_mul(3)))
}
Ok(ratelimited) => {
if ratelimited {
let duration_until_can_send = ctx.ratelimit.read().await.until_can_send();
info!(
ctx,
"smtp got rate limited, waiting for {} until can send again",
duration_to_str(duration_until_can_send)
);
tokio::time::timeout(duration_until_can_send, async {
idle_interrupt_receiver.recv().await.unwrap_or_default()
})
.await
.unwrap_or_default();
continue;
} else {
timeout = None;
}
}
timeout = None;
}
// Fake Idle
@@ -501,41 +505,45 @@ impl Scheduler {
Ok(res)
}
fn maybe_network(&self) {
self.interrupt_inbox(InterruptInfo::new(true));
self.interrupt_mvbox(InterruptInfo::new(true));
self.interrupt_sentbox(InterruptInfo::new(true));
self.interrupt_smtp(InterruptInfo::new(true));
async fn maybe_network(&self) {
join!(
self.interrupt_inbox(InterruptInfo::new(true)),
self.interrupt_mvbox(InterruptInfo::new(true)),
self.interrupt_sentbox(InterruptInfo::new(true)),
self.interrupt_smtp(InterruptInfo::new(true))
);
}
fn maybe_network_lost(&self) {
self.interrupt_inbox(InterruptInfo::new(false));
self.interrupt_mvbox(InterruptInfo::new(false));
self.interrupt_sentbox(InterruptInfo::new(false));
self.interrupt_smtp(InterruptInfo::new(false));
async fn maybe_network_lost(&self) {
join!(
self.interrupt_inbox(InterruptInfo::new(false)),
self.interrupt_mvbox(InterruptInfo::new(false)),
self.interrupt_sentbox(InterruptInfo::new(false)),
self.interrupt_smtp(InterruptInfo::new(false))
);
}
fn interrupt_inbox(&self, info: InterruptInfo) {
self.inbox.interrupt(info);
async fn interrupt_inbox(&self, info: InterruptInfo) {
self.inbox.interrupt(info).await;
}
fn interrupt_mvbox(&self, info: InterruptInfo) {
self.mvbox.interrupt(info);
async fn interrupt_mvbox(&self, info: InterruptInfo) {
self.mvbox.interrupt(info).await;
}
fn interrupt_sentbox(&self, info: InterruptInfo) {
self.sentbox.interrupt(info);
async fn interrupt_sentbox(&self, info: InterruptInfo) {
self.sentbox.interrupt(info).await;
}
fn interrupt_smtp(&self, info: InterruptInfo) {
self.smtp.interrupt(info);
async fn interrupt_smtp(&self, info: InterruptInfo) {
self.smtp.interrupt(info).await;
}
fn interrupt_ephemeral_task(&self) {
async fn interrupt_ephemeral_task(&self) {
self.ephemeral_interrupt_send.try_send(()).ok();
}
fn interrupt_location(&self) {
async fn interrupt_location(&self) {
self.location_interrupt_send.try_send(()).ok();
}
@@ -599,7 +607,7 @@ impl ConnectionState {
Ok(())
}
fn interrupt(&self, info: InterruptInfo) {
async fn interrupt(&self, info: InterruptInfo) {
// Use try_send to avoid blocking on interrupts.
self.idle_interrupt_sender.try_send(info).ok();
}
@@ -633,8 +641,8 @@ impl SmtpConnectionState {
}
/// Interrupt any form of idle.
fn interrupt(&self, info: InterruptInfo) {
self.state.interrupt(info);
async fn interrupt(&self, info: InterruptInfo) {
self.state.interrupt(info).await;
}
/// Shutdown this connection completely.
@@ -678,8 +686,8 @@ impl ImapConnectionState {
}
/// Interrupt any form of idle.
fn interrupt(&self, info: InterruptInfo) {
self.state.interrupt(info);
async fn interrupt(&self, info: InterruptInfo) {
self.state.interrupt(info).await;
}
/// Shutdown this connection completely.

View File

@@ -390,8 +390,7 @@ impl Context {
// =============================================================================================
let watched_folders = get_watched_folder_configs(self).await?;
let incoming_messages = stock_str::incoming_messages(self).await;
ret += &format!("<h3>{}</h3><ul>", incoming_messages);
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
for (folder, state) in &folders_states {
let mut folder_added = false;
@@ -433,8 +432,10 @@ impl Context {
// Your last message was sent successfully
// =============================================================================================
let outgoing_messages = stock_str::outgoing_messages(self).await;
ret += &format!("<h3>{}</h3><ul><li>", outgoing_messages);
ret += &format!(
"<h3>{}</h3><ul><li>",
stock_str::outgoing_messages(self).await
);
let detailed = smtp.get_detailed().await;
ret += &*detailed.to_icon();
ret += " ";
@@ -449,8 +450,10 @@ impl Context {
// =============================================================================================
let domain = tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain;
let storage_on_domain = stock_str::storage_on_domain(self, domain).await;
ret += &format!("<h3>{}</h3><ul>", storage_on_domain);
ret += &format!(
"<h3>{}</h3><ul>",
stock_str::storage_on_domain(self, domain).await
);
let quota = self.quota.read().await;
if let Some(quota) = &*quota {
match &quota.recent {
@@ -470,23 +473,30 @@ impl Context {
info!(self, "connectivity: root name hidden: \"{}\"", root_name);
}
let messages = stock_str::messages(self).await;
let part_of_total_used = stock_str::part_of_total_used(
self,
resource.usage.to_string(),
resource.limit.to_string(),
)
.await;
ret += &match &resource.name {
Atom(resource_name) => {
format!(
"<b>{}:</b> {}",
&*escaper::encode_minimal(resource_name),
part_of_total_used
stock_str::part_of_total_used(
self,
resource.usage.to_string(),
resource.limit.to_string()
)
.await,
)
}
Message => {
format!("<b>{}:</b> {}", part_of_total_used, messages)
format!(
"<b>{}:</b> {}",
stock_str::messages(self).await,
stock_str::part_of_total_used(
self,
resource.usage.to_string(),
resource.limit.to_string()
)
.await,
)
}
Storage => {
// do not use a special title needed for "Storage":
@@ -528,8 +538,7 @@ impl Context {
self.schedule_quota_update().await?;
}
} else {
let not_connected = stock_str::not_connected(self).await;
ret += &format!("<li>{}</li>", not_connected);
ret += &format!("<li>{}</li>", stock_str::not_connected(self).await);
self.schedule_quota_update().await?;
}
ret += "</ul>";

View File

@@ -696,7 +696,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact() {
let mut tcm = TestContextManager::new();
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
assert_eq!(
@@ -910,7 +910,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
let mut tcm = TestContextManager::new();
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -1035,7 +1035,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact_concurrent_calls() -> Result<()> {
let mut tcm = TestContextManager::new();
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
@@ -1066,7 +1066,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_secure_join() -> Result<()> {
let mut tcm = TestContextManager::new();
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;

View File

@@ -342,19 +342,20 @@ impl BobState {
true
}
QrInvite::Group { ref grpid, .. } => {
// This is buggy, result will always be
// This is buggy, is_verified_group will always be
// false since the group is created by receive_imf for
// the very handshake message we're handling now. But
// only after we have returned. It does not impact
// the security invariants of secure-join however.
chat::get_chat_id_by_grpid(context, grpid)
let is_verified_group = chat::get_chat_id_by_grpid(context, grpid)
.await?
.map_or(false, |(_chat_id, is_protected, _blocked)| is_protected)
.map_or(false, |(_chat_id, is_protected, _blocked)| is_protected);
// when joining a non-verified group
// the vg-member-added message may be unencrypted
// when not all group members have keys or prefer encryption.
// So only expect encryption if this is a verified group
is_verified_group
}
};
if vg_expect_encrypted
@@ -500,7 +501,7 @@ impl BobHandshakeMsg {
}
/// The next message expected by [`BobState`] in the setup-contact/secure-join protocol.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SecureJoinStep {
/// Expecting the auth-required message.
///

View File

@@ -64,7 +64,7 @@ impl Smtp {
/// Return true if smtp was connected but is not known to
/// have been successfully used the last 60 seconds
pub fn has_maybe_stale_connection(&self) -> bool {
pub async fn has_maybe_stale_connection(&self) -> bool {
if let Some(last_success) = self.last_success {
SystemTime::now()
.duration_since(last_success)
@@ -77,7 +77,7 @@ impl Smtp {
}
/// Check whether we are connected.
pub fn is_connected(&self) -> bool {
pub async fn is_connected(&self) -> bool {
self.transport
.as_ref()
.map(|t| t.is_connected())
@@ -86,12 +86,12 @@ impl Smtp {
/// Connect using configured parameters.
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
if self.has_maybe_stale_connection() {
if self.has_maybe_stale_connection().await {
info!(context, "Closing stale connection");
self.disconnect().await;
}
if self.is_connected() {
if self.is_connected().await {
return Ok(());
}
@@ -117,7 +117,7 @@ impl Smtp {
addr: &str,
provider_strict_tls: bool,
) -> Result<()> {
if self.is_connected() {
if self.is_connected().await {
warn!(context, "SMTP already connected.");
return Ok(());
}
@@ -477,17 +477,21 @@ pub(crate) async fn send_msg_to_smtp(
}
/// Attempts to send queued MDNs.
async fn send_mdns(context: &Context, connection: &mut Smtp) -> Result<()> {
///
/// Returns true if there are more MDNs to send, but rate limiter does not
/// allow to send them. Returns false if there are no more MDNs to send.
/// If sending an MDN fails, returns an error.
async fn send_mdns(context: &Context, connection: &mut Smtp) -> Result<bool> {
loop {
if !context.ratelimit.read().await.can_send() {
info!(context, "Ratelimiter does not allow sending MDNs now");
return Ok(());
return Ok(true);
}
let more_mdns = send_mdn(context, connection).await?;
if !more_mdns {
// No more MDNs to send.
return Ok(());
return Ok(false);
}
}
}
@@ -496,8 +500,10 @@ async fn send_mdns(context: &Context, connection: &mut Smtp) -> Result<()> {
///
/// Logs and ignores SMTP errors to ensure that a single SMTP message constantly failing to be sent
/// does not block other messages in the queue from being sent.
pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp) -> Result<()> {
let ratelimited = if context.ratelimit.read().await.can_send() {
///
/// Returns true if sending was ratelimited, false otherwise. Errors are propagated to the caller.
pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp) -> Result<bool> {
let mut ratelimited = if context.ratelimit.read().await.can_send() {
// add status updates and sync messages to end of sending queue
context.flush_status_updates().await?;
context.send_sync_msg().await?;
@@ -532,11 +538,12 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
// do not attempt to send MDNs if ratelimited happend before on status-updates/sync:
// instead, let the caller recall this function so that more important status-updates/sync are sent out.
if !ratelimited {
send_mdns(context, connection)
ratelimited = send_mdns(context, connection)
.await
.context("failed to send MDNs")?;
}
Ok(())
Ok(ratelimited)
}
/// Tries to send MDN for message `msg_id` to `contact_id`.

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