mirror of
https://github.com/chatmail/core.git
synced 2026-04-03 05:52:10 +03:00
Compare commits
250 Commits
blob-api
...
fix_sql_or
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dac320a810 | ||
|
|
dce7b90fc2 | ||
|
|
4f94bdff3f | ||
|
|
ce1f2a6fd4 | ||
|
|
da292bb9b2 | ||
|
|
326a75d0e8 | ||
|
|
e47860bc2e | ||
|
|
6212151562 | ||
|
|
8c2b9f9901 | ||
|
|
e9a733a789 | ||
|
|
b2fe723570 | ||
|
|
33ba8dabe0 | ||
|
|
0842e54f52 | ||
|
|
08d34e41c6 | ||
|
|
e93c9f74c9 | ||
|
|
1ab81256e9 | ||
|
|
cb19de57bb | ||
|
|
e678e7df8f | ||
|
|
8487eefe46 | ||
|
|
86da1aa429 | ||
|
|
48b580b59e | ||
|
|
8b568d796e | ||
|
|
4b5af85094 | ||
|
|
8d0be06f45 | ||
|
|
26ae8accd4 | ||
|
|
321e3e27de | ||
|
|
7d26968bb3 | ||
|
|
83464a882e | ||
|
|
1e94ad25e1 | ||
|
|
a3ba19db96 | ||
|
|
d9e9c849e1 | ||
|
|
c162c23d9e | ||
|
|
90fd1c300f | ||
|
|
902a9cc812 | ||
|
|
c51e1805fa | ||
|
|
7a2b9e85e7 | ||
|
|
547c40cd52 | ||
|
|
e2d631097d | ||
|
|
cc55be0b0a | ||
|
|
64927190bd | ||
|
|
24515126fe | ||
|
|
7a56a93028 | ||
|
|
ea7fc3a171 | ||
|
|
ae36a26045 | ||
|
|
d6c9f5c64b | ||
|
|
c4f4f4295b | ||
|
|
a997322efb | ||
|
|
799688af76 | ||
|
|
260e95d027 | ||
|
|
f9ee70aa2e | ||
|
|
50f13cb84b | ||
|
|
fc7e08bb49 | ||
|
|
06ed3e5dfd | ||
|
|
4d792ad57b | ||
|
|
4fa78bfca0 | ||
|
|
2012833cb3 | ||
|
|
e48eef7e32 | ||
|
|
74ac9c3a92 | ||
|
|
a907d789d6 | ||
|
|
fc46c0b49c | ||
|
|
fef7862045 | ||
|
|
d9441a6bdd | ||
|
|
332cb0896b | ||
|
|
d1b0c28924 | ||
|
|
dce958aac4 | ||
|
|
438940219e | ||
|
|
f28fcec81d | ||
|
|
586d027f86 | ||
|
|
bd4fb7486d | ||
|
|
f9cd2b8f36 | ||
|
|
62e22236b7 | ||
|
|
8b157f427a | ||
|
|
f165c1d9b0 | ||
|
|
500e2d62a0 | ||
|
|
a06e8677ac | ||
|
|
b4d5783928 | ||
|
|
3ce7f45503 | ||
|
|
b436c2761a | ||
|
|
b586d3bb0e | ||
|
|
63688a2f95 | ||
|
|
379cb1b2e0 | ||
|
|
78429492f1 | ||
|
|
9875047674 | ||
|
|
5014b0a9cb | ||
|
|
ef841b1aa3 | ||
|
|
368f27ffbc | ||
|
|
0e50bc1443 | ||
|
|
7c4a6ddcdf | ||
|
|
7ab6d95b6c | ||
|
|
6c32b89906 | ||
|
|
056e659a20 | ||
|
|
62baff665c | ||
|
|
7c5eb0ae37 | ||
|
|
36bce6c468 | ||
|
|
65df02163d | ||
|
|
19a32cdfd3 | ||
|
|
d708f386a1 | ||
|
|
0f837f4bed | ||
|
|
242e8e2bb3 | ||
|
|
1d56b24b67 | ||
|
|
bb9138708a | ||
|
|
34f5510f1f | ||
|
|
6c6d47c89c | ||
|
|
196075c031 | ||
|
|
2e5e8f73c6 | ||
|
|
ada5d38272 | ||
|
|
c4b0f773db | ||
|
|
276daf631e | ||
|
|
fb19b58147 | ||
|
|
13a5e3cf6f | ||
|
|
1caf3caf1b | ||
|
|
564370f79a | ||
|
|
24e749a2c9 | ||
|
|
cccdc51ad4 | ||
|
|
99ddce6c3e | ||
|
|
f68088cfb5 | ||
|
|
c8f56d748a | ||
|
|
a43fc47bb6 | ||
|
|
8c1bfac53b | ||
|
|
97853c3660 | ||
|
|
f304a30193 | ||
|
|
7eadca3959 | ||
|
|
8aa6decbf9 | ||
|
|
7cf4bcaca2 | ||
|
|
9ccd9c3e0e | ||
|
|
c6773a6303 | ||
|
|
e858a32aa1 | ||
|
|
99f2680e2c | ||
|
|
7a9a323bac | ||
|
|
62aa234352 | ||
|
|
0cb9e7922a | ||
|
|
e73107006e | ||
|
|
ca389cc6fc | ||
|
|
60ec7f0cbf | ||
|
|
d342d59e65 | ||
|
|
2690fa2da5 | ||
|
|
e411c394ca | ||
|
|
d69f3ba225 | ||
|
|
739807b1a9 | ||
|
|
d029ea7f3f | ||
|
|
11098cb869 | ||
|
|
f6807d6b22 | ||
|
|
7fc9bacf54 | ||
|
|
57ea4c1d92 | ||
|
|
bcdd15ef3a | ||
|
|
5f939c3123 | ||
|
|
2446fc44ad | ||
|
|
9ba8dd91df | ||
|
|
10e1cdbc52 | ||
|
|
46eceb38d5 | ||
|
|
81de882e2f | ||
|
|
593e07cdff | ||
|
|
8ca54f616e | ||
|
|
f7f899f0a4 | ||
|
|
05a3c0c89b | ||
|
|
f21691c122 | ||
|
|
836e26d8d0 | ||
|
|
8a7c1fe4cb | ||
|
|
7f43d3bb37 | ||
|
|
11b975ab19 | ||
|
|
315e4215d9 | ||
|
|
e35e6c44cf | ||
|
|
260cb78e3a | ||
|
|
a1f04d2129 | ||
|
|
9b562eebcd | ||
|
|
1d175c4557 | ||
|
|
f755070080 | ||
|
|
1c6c72a0fe | ||
|
|
1755f2ea3d | ||
|
|
498cc6c80b | ||
|
|
8d3227a92b | ||
|
|
c6d855084e | ||
|
|
827b3f8aeb | ||
|
|
fb95573000 | ||
|
|
f026bd455f | ||
|
|
f0b92a5757 | ||
|
|
d7c6f1e63b | ||
|
|
1e9e308df3 | ||
|
|
c9a70f149d | ||
|
|
d4ff47b6ac | ||
|
|
8b4b241403 | ||
|
|
6dcd6947d7 | ||
|
|
327328412a | ||
|
|
42f9ef00b9 | ||
|
|
8c2ea0fa26 | ||
|
|
14e9afaf42 | ||
|
|
a3a101641a | ||
|
|
8bd93fe495 | ||
|
|
56df22bca7 | ||
|
|
5f32a6738a | ||
|
|
1c081935fb | ||
|
|
8fd4d00776 | ||
|
|
7d04ea58c3 | ||
|
|
2cc84a0f0d | ||
|
|
8f715532cb | ||
|
|
5a77df7cc5 | ||
|
|
59658f2b0b | ||
|
|
0b983906da | ||
|
|
cd1f164d18 | ||
|
|
e2a6ac6625 | ||
|
|
8e8c10c438 | ||
|
|
7ff25f282e | ||
|
|
b8dc608032 | ||
|
|
d7e699320b | ||
|
|
ef333da770 | ||
|
|
575a389b08 | ||
|
|
9bc0824be6 | ||
|
|
b656a60234 | ||
|
|
bd988d805c | ||
|
|
7ad7ccb8fe | ||
|
|
e30c535f18 | ||
|
|
de7706f622 | ||
|
|
de20e4c9dd | ||
|
|
41f9314e2a | ||
|
|
2280ce349a | ||
|
|
7aa05e1c9f | ||
|
|
69d174c9e8 | ||
|
|
3c38fa6b70 | ||
|
|
728c8b4663 | ||
|
|
fab9563cd5 | ||
|
|
20fe2473e1 | ||
|
|
3815062c11 | ||
|
|
581ea9fda0 | ||
|
|
bfa641cea8 | ||
|
|
29c58efeb3 | ||
|
|
27eb82c556 | ||
|
|
652d67a20f | ||
|
|
ce0984f02f | ||
|
|
b3e3b1e245 | ||
|
|
c69ee180af | ||
|
|
bba3a25371 | ||
|
|
095b358aca | ||
|
|
833e5f46cc | ||
|
|
3e0ce0e07a | ||
|
|
1f31dd12fc | ||
|
|
f63efc29bf | ||
|
|
3e394f14e8 | ||
|
|
ff6ffa1656 | ||
|
|
304c259a57 | ||
|
|
630754b52e | ||
|
|
afd8c0d879 | ||
|
|
6316ee7c9b | ||
|
|
b6b8d11881 | ||
|
|
8753fd5887 | ||
|
|
5aaafb5ac1 | ||
|
|
a043557c44 | ||
|
|
4af4914e32 | ||
|
|
e35b3f1e80 | ||
|
|
ff8859b9db | ||
|
|
937ff5a378 |
26
.github/mergeable.yml
vendored
Normal file
26
.github/mergeable.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
version: 2
|
||||
mergeable:
|
||||
- when: pull_request.*
|
||||
name: "Changelog check"
|
||||
validate:
|
||||
- do: or
|
||||
validate:
|
||||
- do: description
|
||||
must_include:
|
||||
regex: '#skip-changelog'
|
||||
- do: and
|
||||
validate:
|
||||
- do: dependent
|
||||
changed:
|
||||
file: 'src/**'
|
||||
required: ['CHANGELOG.md']
|
||||
- do: dependent
|
||||
changed:
|
||||
file: 'deltachat-ffi/**'
|
||||
required: ['CHANGELOG.md']
|
||||
fail:
|
||||
- do: checks
|
||||
status: 'action_required'
|
||||
payload:
|
||||
title: Changlog might need an update
|
||||
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -77,19 +77,19 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version, same as in `rust-toolchain` file.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.54.0
|
||||
rust: 1.59.0
|
||||
python: 3.9
|
||||
- os: windows-latest
|
||||
rust: 1.54.0
|
||||
rust: 1.59.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
|
||||
# Minimum Supported Rust Version = 1.51.0
|
||||
# Minimum Supported Rust Version = 1.56.0
|
||||
#
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.51.0
|
||||
rust: 1.56.0
|
||||
python: 3.7
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
28
.github/workflows/upload-docs.yml
vendored
Normal file
28
.github/workflows/upload-docs.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Build & Deploy Documentation on rs.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- docs-gh-action
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat --no-deps
|
||||
- name: Upload to rs.delta.chat
|
||||
uses: up9cloud/action-rsync@v1.3
|
||||
env:
|
||||
USER: ${{ secrets.USERNAME }}
|
||||
KEY: ${{ secrets.KEY }}
|
||||
HOST: "delta.chat"
|
||||
SOURCE: "target/doc"
|
||||
TARGET: "/var/www/html/rs/"
|
||||
|
||||
28
.github/workflows/upload-ffi-docs.yml
vendored
Normal file
28
.github/workflows/upload-ffi-docs.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Build & Deploy Documentation on cffi.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- docs-gh-action
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat_ffi --no-deps
|
||||
- name: Upload to cffi.delta.chat
|
||||
uses: up9cloud/action-rsync@v1.3
|
||||
env:
|
||||
USER: ${{ secrets.USERNAME }}
|
||||
KEY: ${{ secrets.KEY }}
|
||||
HOST: "delta.chat"
|
||||
SOURCE: "target/doc"
|
||||
TARGET: "/var/www/html/cffi/"
|
||||
|
||||
140
CHANGELOG.md
140
CHANGELOG.md
@@ -3,9 +3,145 @@
|
||||
## Unreleased
|
||||
|
||||
### API changes
|
||||
- change semantics of `dc_get_webxdc_status_updates()` second parameter
|
||||
and remove update-id from `DC_EVENT_WEBXDC_STATUS_UPDATE` #3081
|
||||
|
||||
### Fixes
|
||||
- Hopefully fix a bug where outgoing messages appear twice with Amazon SES #3077
|
||||
- do not delete messages without Message-IDs as duplicates #3095
|
||||
- Assign replies from a different email address to the correct chat #3119
|
||||
- start ephemeral timer when seen status is synchronized via IMAP #3122
|
||||
- do not delete duplicate messages on IMAP immediately to accidentally deleting
|
||||
the last copy #3138
|
||||
|
||||
### Changes
|
||||
- add more SMTP logging #3093
|
||||
- place common headers like `From:` before the large `Autocrypt:` header #3079
|
||||
- keep track of securejoin joiner status in database to survive restarts #2920
|
||||
- remove never used `SentboxMove` option #3111
|
||||
- improve speed by caching config values #3131 #3145
|
||||
- optimize `markseen_msgs` #3141
|
||||
- automatically accept chats with outgoing messages #3143
|
||||
- `dc_receive_imf` refactorings #3154 #3156
|
||||
- add index to speedup deletion of expired ephemeral messages #3155
|
||||
|
||||
|
||||
### Fixes
|
||||
- Fix a bug where sometimes the file extension of a long filename containing a dot was cropped #3098
|
||||
|
||||
## 1.76.0
|
||||
|
||||
### Changes
|
||||
- move messages in batches #3058
|
||||
- delete messages in batches #3060
|
||||
- python: remove arbitrary timeouts from tests #3059
|
||||
- refactorings #3026
|
||||
|
||||
### Fixes
|
||||
- avoid archived, fresh chats #3053
|
||||
- Also resync UIDs in folders that are not configured #2289
|
||||
- treat "NO" IMAP response to MOVE and COPY commands as an error #3058
|
||||
- Fix a bug where messages in the Spam folder created contact requests #3015
|
||||
- Fix a bug where drafts disappeared after some days #3067
|
||||
- Parse MS Exchange read receipts and mark the original message as read #3075
|
||||
- do not retry message sending infinitely in case of permanent SMTP failure #3070
|
||||
- set message state to failed when retry limit is exceeded #3072
|
||||
|
||||
|
||||
## 1.75.0
|
||||
|
||||
### Changes
|
||||
- optimize `delete_expired_imap_messages()` #3047
|
||||
|
||||
|
||||
## 1.74.0
|
||||
|
||||
### Fixes
|
||||
- avoid reconnection loop when message without Message-ID is marked as seen #3044
|
||||
|
||||
|
||||
## 1.73.0
|
||||
|
||||
### API changes
|
||||
- added `only_fetch_mvbox` config #3028
|
||||
|
||||
### Changes
|
||||
- don't watch Sent folder by default #3025
|
||||
- use webxdc app name in chatlist/quotes/replies etc. #3027
|
||||
- make it possible to cancel message sending by removing the message #3034,
|
||||
this was previosuly removed in 1.71.0 #2939
|
||||
- synchronize Seen flags only on watched folders to speed up
|
||||
folder scanning #3041
|
||||
- remove direct dependency on `byteorder` crate #3031
|
||||
- refactorings #3023 #3013
|
||||
- update provider database #3043
|
||||
- improve documentation #3017 #3018 #3021
|
||||
|
||||
### Fixes
|
||||
- fix splitting off text from webxdc messages #3032
|
||||
- call slow `delete_expired_imap_messages()` less often #3037
|
||||
- make synchronization of Seen status more robust in case unsolicited FETCH
|
||||
result without UID is returned #3022
|
||||
- fetch Inbox before scanning folders to ensure iOS does
|
||||
not kill the app before it gets to fetch the Inbox in background #3040
|
||||
|
||||
|
||||
## 1.72.0
|
||||
|
||||
### Fixes
|
||||
- run migrations on backup import #3006
|
||||
|
||||
|
||||
## 1.71.0
|
||||
|
||||
### API Changes
|
||||
- added APIs to handle database passwords: `dc_context_new_closed()`, `dc_context_open()`,
|
||||
`dc_context_is_open()` and `dc_accounts_add_closed_account()` #2956 #2972
|
||||
- use second parameter of `dc_imex` to provide backup passphrase #2980
|
||||
- added `DC_MSG_WEBXDC`, `dc_send_webxdc_status_update()`,
|
||||
`dc_get_webxdc_status_updates()`, `dc_msg_get_webxdc_blob()`, `dc_msg_get_webxdc_info()`
|
||||
and `DC_EVENT_WEBXDC_STATUS_UPDATE` #2826 #2971 #2975 #2977 #2979 #2993 #2994 #2998 #3001 #3003
|
||||
- added `dc_msg_get_parent()` #2984
|
||||
- added `dc_msg_force_plaintext()` API for bots #2847
|
||||
- allow removing quotes on drafts `dc_msg_set_quote(msg, NULL)` #2950
|
||||
- removed `mvbox_watch` option; watching is enabled when `mvbox_move` is enabled #2906
|
||||
- removed `inbox_watch` option #2922
|
||||
- deprecated `os_name` in `dc_context_new()`, pass `NULL` or an empty string #2956
|
||||
|
||||
### Changes
|
||||
- start making it possible to write to mailing lists #2736
|
||||
- add `hop_info` to `dc_get_info()` #2751 #2914 #2923
|
||||
- add information about whether the database is encrypted or not to `dc_get_info()` #3000
|
||||
- selfstatus now defaults to empty #2951 #2960
|
||||
- validate detached cryptographic signatures as used eg. by Thunderbird #2865
|
||||
- do not change the draft's `msg_id` on updates and sending #2887
|
||||
- add `imap` table to keep track of message UIDs #2909 #2938
|
||||
- replace `SendMsgToSmtp` jobs which stored outgoing messages in blobdir with `smtp` SQL table #2939 #2996
|
||||
- sql: enable `auto_vacuum=INCREMENTAL` #2931
|
||||
- sql: build rusqlite with sqlcipher #2934
|
||||
- synchronize Seen status across devices #2942
|
||||
- `dc_preconfigure_keypair` now takes ascii armored keys instead of base64 #2862
|
||||
- put removed member in Bcc instead of To in the message about removal #2864
|
||||
- improve group updates #2889
|
||||
- re-write the blob filename creation loop #2981
|
||||
- update provider database (11 Jan 2022) #2959
|
||||
- python: allow timeout for internal configure tracker API #2967
|
||||
- python: remove API deprecated in Python 3.10 #2907
|
||||
- refactorings #2932 #2957 #2947
|
||||
- improve tests #2863 #2866 #2881 #2908 #2918 #2901 #2973
|
||||
- improve documentation #2880 #2886 #2895
|
||||
- improve ci #2919 #2926 #2969 #2999
|
||||
|
||||
### Fixes
|
||||
- fix leaving groups #2929
|
||||
- fix unread count #2861
|
||||
- make `add_parts()` not early-exit #2879
|
||||
- recognize MS Exchange read receipts as read receipts #2890
|
||||
- create parent directory if creating a new file fails #2978
|
||||
- save "configured" flag later #2974
|
||||
- improve log #2928
|
||||
- `dc_receive_imf`: don't fail on invalid address in the To field #2940
|
||||
|
||||
- Removed `mvbox_watch` option. #2906
|
||||
It is automatically enabled whenever `mvbox_move` is enabled.
|
||||
|
||||
## 1.70.0
|
||||
|
||||
|
||||
763
Cargo.lock
generated
763
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
42
Cargo.toml
42
Cargo.toml
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.70.0"
|
||||
version = "1.76.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
resolver = "2"
|
||||
rust-version = "1.56"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -20,14 +20,13 @@ anyhow = "1"
|
||||
async-imap = { git = "https://github.com/async-email/async-imap" }
|
||||
async-native-tls = { version = "0.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
|
||||
async-std-resolver = "0.20"
|
||||
async-std = { version = "1", features = ["unstable"] }
|
||||
async-std-resolver = "0.21"
|
||||
async-std = { version = "1" }
|
||||
async-tar = { version = "0.4", default-features=false }
|
||||
async-trait = "0.1"
|
||||
backtrace = "0.3"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.3"
|
||||
byteorder = "1.3"
|
||||
chrono = "0.4"
|
||||
dirs = { version = "4", optional=true }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
@@ -35,26 +34,26 @@ encoded-words = { git = "https://github.com/async-email/encoded-words", branch="
|
||||
escaper = "0.1"
|
||||
futures = "0.3"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.23.5", 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"
|
||||
log = {version = "0.4.8", optional = true }
|
||||
log = {version = "0.4.16", optional = true }
|
||||
mailparse = "0.13"
|
||||
native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.9.0"
|
||||
once_cell = "1.10.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.7", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.22"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.19"
|
||||
r2d2_sqlite = "0.20"
|
||||
rand = "0.7"
|
||||
regex = "1.5"
|
||||
rusqlite = { version = "0.26", features = ["sqlcipher"] }
|
||||
rusqlite = { version = "0.27", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "9", optional = true }
|
||||
sanitize-filename = "0.3"
|
||||
@@ -63,9 +62,9 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1"
|
||||
stop-token = "0.6"
|
||||
strum = "0.23"
|
||||
strum_macros = "0.23"
|
||||
stop-token = "0.7"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
@@ -74,13 +73,14 @@ uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
fast-socks5 = "0.4"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "3.3.0"
|
||||
textwrap = "0.14.2"
|
||||
tagger = "4.3.3"
|
||||
textwrap = "0.15.0"
|
||||
zip = { version = "0.6.0", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
async-std = { version = "1", features = ["unstable", "attributes"] }
|
||||
criterion = "0.3"
|
||||
criterion = { version = "0.3.4", features = ["async_std"] }
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
@@ -116,6 +116,14 @@ harness = false
|
||||
name = "search_msgs"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "receive_emails"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "get_chat_msgs"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
|
||||
12
README.md
12
README.md
@@ -12,6 +12,8 @@ To download and install the official compiler for the Rust programming language,
|
||||
$ curl https://sh.rustup.rs -sSf | sh
|
||||
```
|
||||
|
||||
> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
|
||||
|
||||
## Using the CLI client
|
||||
|
||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||
@@ -125,11 +127,11 @@ $ cargo test -- --ignored
|
||||
|
||||
Language bindings are available for:
|
||||
|
||||
- [C](https://c.delta.chat)
|
||||
- [Node.js](https://www.npmjs.com/package/deltachat-node)
|
||||
- [Python](https://py.delta.chat)
|
||||
- [Go](https://github.com/deltachat/go-deltachat/)
|
||||
- [Free Pascal](https://github.com/deltachat/deltachat-fp/)
|
||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||
- **Node.js** \[[📂 source](https://github.com/deltachat/deltachat-node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||
- **Go** \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
||||
- **Free Pascal** \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||
|
||||
The following "frontend" projects make use of the Rust-library
|
||||
|
||||
BIN
assets/icon-webxdc.png
Normal file
BIN
assets/icon-webxdc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
181
assets/icon-webxdc.svg
Normal file
181
assets/icon-webxdc.svg
Normal file
@@ -0,0 +1,181 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="80mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 80 297"
|
||||
version="1.1"
|
||||
id="svg71"
|
||||
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
|
||||
sodipodi:docname="icon-webxdc.svg"
|
||||
inkscape:export-filename="C:\Users\user\OneDrive - BFW-Leipzig\Documents\LogoDC\finalohnerand.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<metadata
|
||||
id="metadata856">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
id="namedview73"
|
||||
pagecolor="#767676"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
showborder="false"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:snap-bbox-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:zoom="1.4142136"
|
||||
inkscape:cx="-90.271136"
|
||||
inkscape:cy="-1233.1209"
|
||||
inkscape:window-width="1864"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="56"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:snap-global="false"
|
||||
showguides="false"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:document-rotation="0"
|
||||
units="px">
|
||||
<sodipodi:guide
|
||||
position="-154.76097,641.11689"
|
||||
orientation="0,-1"
|
||||
id="guide21118" />
|
||||
<sodipodi:guide
|
||||
position="-60.286487,633.36619"
|
||||
orientation="0,-1"
|
||||
id="guide21120" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs68">
|
||||
<linearGradient
|
||||
id="linearGradient4375">
|
||||
<stop
|
||||
style="stop-color:#364e59;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4377" />
|
||||
<stop
|
||||
style="stop-color:#364e59;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop4379" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#1a1a1a;stroke:#000000;stroke-width:0.167903"
|
||||
id="rect880"
|
||||
width="79.8321"
|
||||
height="79.8321"
|
||||
x="-64.03286"
|
||||
y="-375.9097"
|
||||
ry="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3799-2"
|
||||
d="m -24.089585,-372.59579 c -19.986026,0.24336 -36.196903,16.666 -36.196903,36.67011 0,20.00409 16.210877,36.03233 36.196903,35.78912 19.0024236,-0.076 14.5340713,-10.6146 35.538854,-0.85693 -11.50627538,-17.97454 0.390097,-20.36737 0.658079,-35.81316 0,-20.00411 -16.2108788,-36.03235 -36.196911,-35.78914 z"
|
||||
style="fill:#364e59;fill-opacity:1;stroke:none;stroke-width:1.93355"
|
||||
sodipodi:nodetypes="sscccs" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M -54.193871,-325.26419 Z"
|
||||
id="path3846" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M -49.397951,-326.67773 Z"
|
||||
id="path3848" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m -49.397951,-326.67773 v 0 0"
|
||||
id="path3850" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.01;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m -51.35133,-325.0334 -7.964067,5.98895 z"
|
||||
id="path3965" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path11037"
|
||||
d="m -24.089585,-372.19891 c -19.986026,0.24156 -36.196903,16.54352 -36.196903,36.40062 0,7.86524 2.543315,15.1113 6.857155,20.97971 6.577146,8.94734 11.123515,9.77363 11.123515,9.77363 1.343237,1.78324 10.270932,4.3223 10.270932,4.3223 l 16.791727,-70.86654 -0.468369,-0.33457 c 0.458597,0.26445 0.428277,-0.27515 -8.378035,-0.27515 z"
|
||||
style="fill:#7cc5cc;fill-opacity:1;stroke:none;stroke-width:1.92643"
|
||||
sodipodi:nodetypes="sssccccss" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M -49.944239,-310.69957 Z"
|
||||
id="path13674" />
|
||||
<g
|
||||
id="g15178"
|
||||
transform="matrix(0.79975737,0,0,0.79975737,53.088959,-63.716396)">
|
||||
<rect
|
||||
style="fill:#364e59;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="-334.4964"
|
||||
y="-154.51025"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#364e59;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072-5"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="147.63107"
|
||||
y="-334.4964"
|
||||
transform="rotate(-45)"
|
||||
inkscape:transform-center-x="-0.74835017"
|
||||
inkscape:transform-center-y="0.37417525" />
|
||||
</g>
|
||||
<g
|
||||
id="g22468"
|
||||
transform="translate(3.3033974)">
|
||||
<g
|
||||
id="g15178-0"
|
||||
transform="matrix(-0.79975737,0,0,0.79975737,-103.11028,-63.716404)"
|
||||
style="fill:#7cc5cc;fill-opacity:1">
|
||||
<rect
|
||||
style="fill:#7cc5cc;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072-2"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="-334.4964"
|
||||
y="-154.51025"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#7cc5cc;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072-5-5"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="147.63107"
|
||||
y="-334.4964"
|
||||
transform="rotate(-45)"
|
||||
inkscape:transform-center-x="-0.74835017"
|
||||
inkscape:transform-center-y="0.37417525" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
38
benches/get_chat_msgs.rs
Normal file
38
benches/get_chat_msgs.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use async_std::path::Path;
|
||||
|
||||
use criterion::async_executor::AsyncStdExecutor;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
|
||||
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
|
||||
for c in chats.iter().take(10) {
|
||||
black_box(chat::get_chat_msgs(&context, *c, 0, None).await.ok());
|
||||
}
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
||||
// messages, such as your primary account.
|
||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
||||
let chats: Vec<_> = async_std::task::block_on(async {
|
||||
let context = Context::new((&path).into(), 100).await.unwrap();
|
||||
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
|
||||
let len = chatlist.len();
|
||||
(0..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
|
||||
});
|
||||
|
||||
c.bench_function("Load all chats", |b| {
|
||||
b.to_async(AsyncStdExecutor)
|
||||
.iter(|| get_chat_msgs_benchmark(black_box(&path.as_ref()), black_box(&chats)))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
84
benches/receive_emails.rs
Normal file
84
benches/receive_emails.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use async_std::{path::PathBuf, task::block_on};
|
||||
use criterion::{
|
||||
async_executor::AsyncStdExecutor, black_box, criterion_group, criterion_main, BatchSize,
|
||||
Criterion,
|
||||
};
|
||||
use deltachat::{
|
||||
config::Config,
|
||||
context::Context,
|
||||
dc_receive_imf::dc_receive_imf,
|
||||
imex::{imex, ImexMode},
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn recv_all_emails(context: Context) -> Context {
|
||||
for i in 0..100 {
|
||||
let imf_raw = format!(
|
||||
"Subject: Benchmark
|
||||
Message-ID: Mr.OssSYnOFkhR.{i}@testrun.org
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||
To: alice@example.com
|
||||
From: sender@testrun.org
|
||||
Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: sender@testrun.org
|
||||
Chat-User-Avatar: 0
|
||||
In-Reply-To: Mr.OssSYnOFkhR.{i_dec}@testrun.org
|
||||
MIME-Version: 1.0
|
||||
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
Hello {i}",
|
||||
i = i,
|
||||
i_dec = i - 1,
|
||||
);
|
||||
dc_receive_imf(&context, black_box(imf_raw.as_bytes()), "INBOX", false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
context
|
||||
}
|
||||
|
||||
async fn create_context() -> Context {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
|
||||
let backup: PathBuf = std::env::current_dir()
|
||||
.unwrap()
|
||||
.join("delta-chat-backup.tar")
|
||||
.into();
|
||||
if backup.exists().await {
|
||||
println!("Importing backup");
|
||||
imex(&context, ImexMode::ImportBackup, &backup, None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let addr = "alice@example.com";
|
||||
context.set_config(Config::Addr, Some(addr)).await.unwrap();
|
||||
context
|
||||
.set_config(Config::ConfiguredAddr, Some(addr))
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.set_config(Config::Configured, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Receive messages");
|
||||
group.bench_function("Receive 100 simple text msgs", |b| {
|
||||
b.to_async(AsyncStdExecutor).iter_batched(
|
||||
|| block_on(create_context()),
|
||||
|context| recv_all_emails(black_box(context)),
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.70.0"
|
||||
version = "1.76.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Delta Chat C Interface
|
||||
|
||||
## Installation
|
||||
|
||||
see `Installing libdeltachat system wide` in [../README.md](../README.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
To generate the C Interface documentation,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,15 +27,17 @@ use async_std::sync::RwLock;
|
||||
use async_std::task::{block_on, spawn};
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
use rand::Rng;
|
||||
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, Origin};
|
||||
use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
|
||||
@@ -75,7 +77,6 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
}
|
||||
|
||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||
use rand::Rng;
|
||||
// generate random ID as this functionality is not yet available on the C-api.
|
||||
let id = rand::thread_rng().gen();
|
||||
block_on(Context::new(as_path(dbfile).to_path_buf().into(), id))
|
||||
@@ -86,12 +87,63 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
match ctx {
|
||||
Ok(ctx) => Box::into_raw(Box::new(ctx)),
|
||||
Err(err) => {
|
||||
eprintln!("failed to create context: {}", err);
|
||||
eprintln!("failed to create context: {:#}", err);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *mut dc_context_t {
|
||||
setup_panic!();
|
||||
|
||||
if dbfile.is_null() {
|
||||
eprintln!("ignoring careless call to dc_context_new_closed()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let id = rand::thread_rng().gen();
|
||||
match block_on(Context::new_closed(
|
||||
as_path(dbfile).to_path_buf().into(),
|
||||
id,
|
||||
)) {
|
||||
Ok(context) => Box::into_raw(Box::new(context)),
|
||||
Err(err) => {
|
||||
eprintln!("failed to create context: {:#}", err);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_open(
|
||||
context: *mut dc_context_t,
|
||||
passphrase: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_context_open()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let ctx = &*context;
|
||||
let passphrase = to_string_lossy(passphrase);
|
||||
block_on(ctx.open(passphrase))
|
||||
.log_err(ctx, "dc_context_open() failed")
|
||||
.map(|b| b as libc::c_int)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_context_is_open()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let ctx = &*context;
|
||||
block_on(ctx.is_open()) as libc::c_int
|
||||
}
|
||||
|
||||
/// Release the context structure.
|
||||
///
|
||||
/// This function releases the memory of the `dc_context_t` structure.
|
||||
@@ -441,14 +493,17 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
|
||||
let id = id.unwrap_or_default();
|
||||
id as libc::c_int
|
||||
id.to_u32() as libc::c_int
|
||||
}
|
||||
EventType::ConfigureProgress { progress, .. } | EventType::ImexProgress(progress) => {
|
||||
*progress as libc::c_int
|
||||
}
|
||||
EventType::ImexFileWritten(_) => 0,
|
||||
EventType::SecurejoinInviterProgress { contact_id, .. }
|
||||
| EventType::SecurejoinJoinerProgress { contact_id, .. } => *contact_id as libc::c_int,
|
||||
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
|
||||
contact_id.to_u32() as libc::c_int
|
||||
}
|
||||
EventType::WebxdcStatusUpdate(msg_id) => msg_id.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,6 +536,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::MsgsNoticed(_)
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::WebxdcStatusUpdate(_)
|
||||
| EventType::ChatModified(_) => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
@@ -531,6 +587,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::SecurejoinJoinerProgress { .. }
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::WebxdcStatusUpdate(_)
|
||||
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
@@ -660,7 +717,11 @@ pub unsafe extern "C" fn dc_get_chatlist(
|
||||
let ctx = &*context;
|
||||
let qs = to_opt_string_lossy(query_str);
|
||||
|
||||
let qi = if query_id == 0 { None } else { Some(query_id) };
|
||||
let qi = if query_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(ContactId::new(query_id))
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
match chatlist::Chatlist::try_load(ctx, flags as usize, qs.as_deref(), qi)
|
||||
@@ -688,7 +749,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
ChatId::create_for_contact(ctx, contact_id)
|
||||
ChatId::create_for_contact(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.log_err(ctx, "Failed to create chat from contact_id")
|
||||
.map(|id| id.to_u32())
|
||||
@@ -708,7 +769,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
ChatId::lookup_by_contact(ctx, contact_id)
|
||||
ChatId::lookup_by_contact(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.log_err(ctx, "Failed to get chat for contact_id")
|
||||
.unwrap_or_default() // unwraps the Result
|
||||
@@ -820,6 +881,48 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
json: *const libc::c_char,
|
||||
descr: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_webxdc_status_update()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(ctx.send_webxdc_status_update(
|
||||
MsgId::new(msg_id),
|
||||
&to_string_lossy(json),
|
||||
&to_string_lossy(descr),
|
||||
))
|
||||
.log_err(ctx, "Failed to send webxdc update")
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_webxdc_status_updates(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
last_known_serial: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_webxdc_status_updates()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(ctx.get_webxdc_status_updates(
|
||||
MsgId::new(msg_id),
|
||||
StatusUpdateSerial::new(last_known_serial),
|
||||
))
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1243,7 +1346,10 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
|
||||
let arr = dc_array_t::from(
|
||||
chat::get_chat_contacts(ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "Failed get_chat_contacts"),
|
||||
.unwrap_or_log_default(ctx, "Failed get_chat_contacts")
|
||||
.iter()
|
||||
.map(|id| id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
})
|
||||
@@ -1353,7 +1459,7 @@ pub unsafe extern "C" fn dc_is_contact_in_chat(
|
||||
block_on(chat::is_contact_in_chat(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
contact_id,
|
||||
ContactId::new(contact_id),
|
||||
))
|
||||
.log_err(ctx, "is_contact_in_chat failed")
|
||||
.unwrap_or_default() as libc::c_int
|
||||
@@ -1374,7 +1480,7 @@ pub unsafe extern "C" fn dc_add_contact_to_chat(
|
||||
block_on(chat::add_contact_to_chat(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
contact_id,
|
||||
ContactId::new(contact_id),
|
||||
))
|
||||
.log_err(ctx, "Failed to add contact")
|
||||
.is_ok() as libc::c_int
|
||||
@@ -1395,7 +1501,7 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat(
|
||||
block_on(chat::remove_contact_from_chat(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
contact_id,
|
||||
ContactId::new(contact_id),
|
||||
))
|
||||
.log_err(ctx, "Failed to remove contact")
|
||||
.is_ok() as libc::c_int
|
||||
@@ -1730,7 +1836,8 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
|
||||
Contact::lookup_id_by_addr(ctx, &to_string_lossy(addr), Origin::IncomingReplyTo)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to lookup id")
|
||||
.unwrap_or(0)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1750,6 +1857,7 @@ pub unsafe extern "C" fn dc_create_contact(
|
||||
block_on(async move {
|
||||
Contact::create(ctx, &name, &to_string_lossy(addr))
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
@@ -1788,7 +1896,9 @@ pub unsafe extern "C" fn dc_get_contacts(
|
||||
|
||||
block_on(async move {
|
||||
match Contact::get_all(ctx, flags, query).await {
|
||||
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(contacts))),
|
||||
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(
|
||||
contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>(),
|
||||
))),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
})
|
||||
@@ -1825,7 +1935,10 @@ pub unsafe extern "C" fn dc_get_blocked_contacts(
|
||||
Contact::get_all_blocked(ctx)
|
||||
.await
|
||||
.log_err(ctx, "Can't get blocked contacts")
|
||||
.unwrap_or_default(),
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|id| id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
)))
|
||||
})
|
||||
}
|
||||
@@ -1836,18 +1949,18 @@ pub unsafe extern "C" fn dc_block_contact(
|
||||
contact_id: u32,
|
||||
block: libc::c_int,
|
||||
) {
|
||||
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL as u32 {
|
||||
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL.to_u32() {
|
||||
eprintln!("ignoring careless call to dc_block_contact()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
if block == 0 {
|
||||
Contact::unblock(ctx, contact_id)
|
||||
Contact::unblock(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.ok_or_log_msg(ctx, "Can't unblock contact");
|
||||
} else {
|
||||
Contact::block(ctx, contact_id)
|
||||
Contact::block(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.ok_or_log_msg(ctx, "Can't block contact");
|
||||
}
|
||||
@@ -1866,7 +1979,7 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
Contact::get_encrinfo(ctx, contact_id)
|
||||
Contact::get_encrinfo(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.map(|s| s.strdup())
|
||||
.unwrap_or_else(|e| {
|
||||
@@ -1881,14 +1994,14 @@ pub unsafe extern "C" fn dc_delete_contact(
|
||||
context: *mut dc_context_t,
|
||||
contact_id: u32,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL as u32 {
|
||||
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL.to_u32() {
|
||||
eprintln!("ignoring careless call to dc_delete_contact()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match Contact::delete(ctx, contact_id).await {
|
||||
match Contact::delete(ctx, ContactId::new(contact_id)).await {
|
||||
Ok(_) => 1,
|
||||
Err(_) => 0,
|
||||
}
|
||||
@@ -1907,7 +2020,7 @@ pub unsafe extern "C" fn dc_get_contact(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
Contact::get_by_id(ctx, contact_id)
|
||||
Contact::get_by_id(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.map(|contact| Box::into_raw(Box::new(ContactWrapper { context, contact })))
|
||||
.unwrap_or_else(|_| ptr::null_mut())
|
||||
@@ -1919,7 +2032,7 @@ pub unsafe extern "C" fn dc_imex(
|
||||
context: *mut dc_context_t,
|
||||
what_raw: libc::c_int,
|
||||
param1: *const libc::c_char,
|
||||
_param2: *const libc::c_char,
|
||||
param2: *const libc::c_char,
|
||||
) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_imex()");
|
||||
@@ -1932,12 +2045,13 @@ pub unsafe extern "C" fn dc_imex(
|
||||
return;
|
||||
}
|
||||
};
|
||||
let passphrase = to_opt_string_lossy(param2);
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
if let Some(param1) = to_opt_string_lossy(param1) {
|
||||
spawn(async move {
|
||||
imex::imex(ctx, what, param1.as_ref())
|
||||
imex::imex(ctx, what, param1.as_ref(), passphrase)
|
||||
.await
|
||||
.log_err(ctx, "IMEX failed")
|
||||
});
|
||||
@@ -2329,7 +2443,7 @@ pub unsafe extern "C" fn dc_array_get_contact_id(
|
||||
return 0;
|
||||
}
|
||||
|
||||
(*array).get_location(index).contact_id
|
||||
(*array).get_location(index).contact_id.to_u32()
|
||||
}
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_array_get_msg_id(
|
||||
@@ -2443,7 +2557,14 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id(
|
||||
return 0;
|
||||
}
|
||||
let ffi_list = &*chatlist;
|
||||
ffi_list.list.get_chat_id(index as usize).to_u32()
|
||||
let ctx = &*ffi_list.context;
|
||||
match ffi_list.list.get_chat_id(index as usize) {
|
||||
Ok(chat_id) => chat_id.to_u32(),
|
||||
Err(err) => {
|
||||
warn!(ctx, "get_chat_id failed: {}", err);
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2838,7 +2959,7 @@ pub unsafe extern "C" fn dc_msg_get_from_id(msg: *mut dc_msg_t) -> u32 {
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_from_id()
|
||||
ffi_msg.message.get_from_id().to_u32()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2960,6 +3081,61 @@ pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c
|
||||
ffi_msg.message.get_filename().unwrap_or_default().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_webxdc_blob(
|
||||
msg: *mut dc_msg_t,
|
||||
filename: *const libc::c_char,
|
||||
ret_bytes: *mut libc::size_t,
|
||||
) -> *mut libc::c_char {
|
||||
if msg.is_null() || filename.is_null() || ret_bytes.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_webxdc_blob()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
let blob = block_on(async move {
|
||||
ffi_msg
|
||||
.message
|
||||
.get_webxdc_blob(ctx, &to_string_lossy(filename))
|
||||
.await
|
||||
});
|
||||
match blob {
|
||||
Ok(blob) => {
|
||||
*ret_bytes = blob.len();
|
||||
let ptr = libc::malloc(*ret_bytes);
|
||||
libc::memcpy(ptr, blob.as_ptr() as *mut libc::c_void, *ret_bytes);
|
||||
ptr as *mut libc::c_char
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("failed read blob from archive: {}", err);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_webxdc_info()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
block_on(async move {
|
||||
let info = match ffi_msg.message.get_webxdc_info(ctx).await {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
error!(ctx, "dc_msg_get_webxdc_info() failed to get info: {}", err);
|
||||
return "".strdup();
|
||||
}
|
||||
};
|
||||
serde_json::to_string(&info)
|
||||
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
|
||||
.strdup()
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
@@ -3372,17 +3548,21 @@ pub unsafe extern "C" fn dc_msg_set_quote(msg: *mut dc_msg_t, quote: *const dc_m
|
||||
return;
|
||||
}
|
||||
let ffi_msg = &mut *msg;
|
||||
let ffi_quote = &*quote;
|
||||
|
||||
if ffi_msg.context != ffi_quote.context {
|
||||
eprintln!("ignoring attempt to quote message from a different context");
|
||||
return;
|
||||
}
|
||||
let quote_msg = if quote.is_null() {
|
||||
None
|
||||
} else {
|
||||
let ffi_quote = &*quote;
|
||||
if ffi_msg.context != ffi_quote.context {
|
||||
eprintln!("ignoring attempt to quote message from a different context");
|
||||
return;
|
||||
}
|
||||
Some(&ffi_quote.message)
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
ffi_msg
|
||||
.message
|
||||
.set_quote(&*ffi_msg.context, &ffi_quote.message)
|
||||
.set_quote(&*ffi_msg.context, quote_msg)
|
||||
.await
|
||||
.log_err(&*ffi_msg.context, "failed to set quote")
|
||||
.ok();
|
||||
@@ -3425,6 +3605,29 @@ pub unsafe extern "C" fn dc_msg_get_quoted_msg(msg: *const dc_msg_t) -> *mut dc_
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_parent(msg: *const dc_msg_t) -> *mut dc_msg_t {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_parent()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ffi_msg: &MessageWrapper = &*msg;
|
||||
let context = &*ffi_msg.context;
|
||||
let res = block_on(async move {
|
||||
ffi_msg
|
||||
.message
|
||||
.parent(context)
|
||||
.await
|
||||
.log_err(context, "failed to get parent message")
|
||||
.unwrap_or(None)
|
||||
});
|
||||
|
||||
match res {
|
||||
Some(message) => Box::into_raw(Box::new(MessageWrapper { context, message })),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_force_plaintext(msg: *mut dc_msg_t) {
|
||||
if msg.is_null() {
|
||||
@@ -3467,7 +3670,7 @@ pub unsafe extern "C" fn dc_contact_get_id(contact: *mut dc_contact_t) -> u32 {
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_id()
|
||||
ffi_contact.contact.get_id().to_u32()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3954,6 +4157,30 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accounts_t) -> u32 {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_add_closed_account()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
match accounts.add_closed_account().await {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
accounts.emit_event(EventType::Error(format!(
|
||||
"Failed to add account: {:#}",
|
||||
err
|
||||
)));
|
||||
0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_remove_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
|
||||
@@ -111,19 +111,19 @@ impl Lot {
|
||||
match self {
|
||||
Self::Summary(_) => Default::default(),
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
||||
Qr::FprOk { contact_id } => *contact_id,
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default(),
|
||||
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Addr { contact_id } => *contact_id,
|
||||
Qr::Addr { contact_id } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
Qr::WithdrawVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
||||
Qr::ReviveVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
||||
},
|
||||
Self::Error(_) => Default::default(),
|
||||
|
||||
181
draft/webxdc-dev-reference.md
Normal file
181
draft/webxdc-dev-reference.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# Webxdc Developer Reference
|
||||
|
||||
## Webxdc File Format
|
||||
|
||||
- a **Webxdc app** is a **ZIP-file** with the extension `.xdc`
|
||||
- the ZIP-file must use the default compression methods as of RFC 1950,
|
||||
this is "Deflate" or "Store"
|
||||
- the ZIP-file must contain at least the file `index.html`
|
||||
- if the Webxdc app is started, `index.html` is opened in a restricted webview
|
||||
that allow accessing resources only from the ZIP-file
|
||||
|
||||
|
||||
## Webxdc API
|
||||
|
||||
There are some additional APIs available once `webxdc.js` is included
|
||||
(the file will be provided by the concrete implementations,
|
||||
no need to add `webxdc.js` to your ZIP-file):
|
||||
|
||||
```html
|
||||
<script src="webxdc.js"></script>
|
||||
```
|
||||
|
||||
### sendUpdate()
|
||||
|
||||
```js
|
||||
window.webxdc.sendUpdate(update, descr);
|
||||
```
|
||||
|
||||
Webxdc apps are usually shared in a chat and run independently on each peer.
|
||||
To get a shared state, the peers use `sendUpdate()` to send updates to each other.
|
||||
|
||||
- `update`: an object with the following properties:
|
||||
- `update.payload`: any javascript primitive, array or object.
|
||||
- `update.info`: optional, short, informational message that will be added to the chat,
|
||||
eg. "Alice voted" or "Bob scored 123 in MyGame";
|
||||
usually only one line of text is shown,
|
||||
use this option sparingly to not spam the chat.
|
||||
- `update.summary`: optional, short text, shown beside app icon;
|
||||
it is recommended to use some aggregated value, eg. "8 votes", "Highscore: 123"
|
||||
|
||||
- `descr`: short, human-readable description what this update is about.
|
||||
this is shown eg. as a fallback text in an email program.
|
||||
|
||||
All peers, including the sending one,
|
||||
will receive the update by the callback given to `setUpdateListener()`.
|
||||
|
||||
There are situations where the user cannot send messages to a chat,
|
||||
eg. if the webxdc instance comes as a contact request or if the user has left a group.
|
||||
In these cases, you can still call `sendUpdate()`,
|
||||
however, the update won't be sent to other peers
|
||||
and you won't get the update by `setUpdateListener()`.
|
||||
|
||||
|
||||
### setUpdateListener()
|
||||
|
||||
```js
|
||||
window.webxdc.setUpdateListener((update) => {}, serial);
|
||||
```
|
||||
|
||||
With `setUpdateListener()` you define a callback that receives the updates
|
||||
sent by `sendUpdate()`. The callback is called for updates sent by you or other peers.
|
||||
The `serial` specifies the last serial that you know about (defaults to 0).
|
||||
|
||||
Each `update` which is passed to the callback comes with the following properties:
|
||||
|
||||
- `update.payload`: equals the payload given to `sendUpdate()`
|
||||
|
||||
- `update.serial`: the serial number of this update.
|
||||
Serials are larger `0` and newer serials have higher numbers.
|
||||
There may be gaps in the serials
|
||||
and it is not guaranteed that the next serial is exactly incremented by one.
|
||||
|
||||
- `update.max_serial`: the maximum serial currently known.
|
||||
If `max_serial` equals `serial` this update is the last update (until new network messages arrive).
|
||||
|
||||
- `update.info`: optional, short, informational message (see `send_update`)
|
||||
|
||||
- `update.summary`: optional, short text, shown beside app icon (see `send_update`)
|
||||
|
||||
|
||||
### selfAddr
|
||||
|
||||
```js
|
||||
window.webxdc.selfAddr
|
||||
```
|
||||
|
||||
Property with the peer's own address.
|
||||
This is esp. useful if you want to differ between different peers -
|
||||
just send the address along with the payload,
|
||||
and, if needed, compare the payload addresses against selfAddr() later on.
|
||||
|
||||
|
||||
### selfName
|
||||
|
||||
```js
|
||||
window.webxdc.selfName
|
||||
```
|
||||
|
||||
Property with the peer's own name.
|
||||
This is name chosen by the user in their settings,
|
||||
if there is nothing set, that defaults to the peer's address.
|
||||
|
||||
|
||||
## manifest.toml
|
||||
|
||||
If the ZIP-file contains a `manifest.toml` in its root directory,
|
||||
some basic information are read and used from there.
|
||||
|
||||
the `manifest.toml` has the following format
|
||||
|
||||
```toml
|
||||
name = "My App Name"
|
||||
```
|
||||
|
||||
- **name** - The name of the app.
|
||||
If no name is set or if there is no manifest, the filename is used as the app name.
|
||||
|
||||
|
||||
## App Icon
|
||||
|
||||
If the ZIP-root contains an `icon.png` or `icon.jpg`,
|
||||
these files are used as the icon for the app.
|
||||
The icon should be a square at reasonable width/height;
|
||||
round corners etc. will be added by the implementations as needed.
|
||||
If no icon is set, a default icon will be used.
|
||||
|
||||
|
||||
## Webxdc Examples
|
||||
|
||||
The following example shows an input field and every input is show on all peers.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<script src="webxdc.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<input id="input" type="text"/>
|
||||
<a href="" onclick="sendMsg(); return false;">Send</a>
|
||||
<p id="output"></p>
|
||||
<script>
|
||||
|
||||
function sendMsg() {
|
||||
msg = document.getElementById("input").value;
|
||||
window.webxdc.sendUpdate({payload: msg}, 'Someone typed "'+msg+'".');
|
||||
}
|
||||
|
||||
function receiveUpdate(update) {
|
||||
document.getElementById('output').innerHTML += update.payload + "<br>";
|
||||
}
|
||||
|
||||
window.webxdc.setUpdateListener(receiveUpdate, 0);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
[Webxdc Development Tool](https://github.com/deltachat/webxdc-dev)
|
||||
offers an **Webxdc Simulator** that can be used in many browsers without any installation needed.
|
||||
You can also use that repository as a template for your own app -
|
||||
just clone and start adapting things to your need.
|
||||
|
||||
|
||||
### Advanced Examples
|
||||
|
||||
- [2048](https://github.com/adbenitez/2048.xdc)
|
||||
- [Draw](https://github.com/adbenitez/draw.xdc)
|
||||
- [Poll](https://github.com/r10s/webxdc-poll/)
|
||||
- [Tic Tac Toe](https://github.com/Simon-Laux/tictactoe.xdc)
|
||||
- Even more with [Topic #webxdc on Github](https://github.com/topics/webxdc)
|
||||
|
||||
|
||||
## Closing Remarks
|
||||
|
||||
- older devices might not have the newest js features in their webview,
|
||||
you may want to transpile your code down to an older js version eg. with https://babeljs.io
|
||||
- there are tons of ideas for enhancements of the API and the file format,
|
||||
eg. in the future, we will may define icon- and manifest-files,
|
||||
allow to aggregate the state or add metadata.
|
||||
@@ -17,7 +17,7 @@ use deltachat::download::DownloadState;
|
||||
use deltachat::imex::*;
|
||||
use deltachat::location;
|
||||
use deltachat::log::LogExt;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId};
|
||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::sql;
|
||||
@@ -84,6 +84,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context.sql().config_cache().write().await.clear();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM leftgrps;", paramsv![])
|
||||
@@ -209,7 +210,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
contact_id,
|
||||
msgtext.unwrap_or_default(),
|
||||
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
||||
if msg.get_from_id() == 1 {
|
||||
if msg.get_from_id() == DC_CONTACT_ID_SELF {
|
||||
""
|
||||
} else if msg.get_state() == MessageState::InSeen {
|
||||
"[SEEN]"
|
||||
@@ -267,9 +268,8 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
|
||||
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
|
||||
for contact_id in contacts {
|
||||
let line;
|
||||
let mut line2 = "".to_string();
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let name = contact.get_display_name();
|
||||
@@ -284,24 +284,20 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
|
||||
} else {
|
||||
""
|
||||
};
|
||||
line = format!(
|
||||
let line = format!(
|
||||
"{}{} <{}>",
|
||||
if !name.is_empty() {
|
||||
&name
|
||||
name
|
||||
} else {
|
||||
"<name unset>"
|
||||
},
|
||||
verified_str,
|
||||
if !addr.is_empty() {
|
||||
&addr
|
||||
} else {
|
||||
"addr unset"
|
||||
}
|
||||
if !addr.is_empty() { addr } else { "addr unset" }
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &addr)
|
||||
let peerstate = Peerstate::from_addr(context, addr)
|
||||
.await
|
||||
.expect("peerstate error");
|
||||
if peerstate.is_some() && *contact_id != 1 {
|
||||
if peerstate.is_some() && *contact_id != DC_CONTACT_ID_SELF {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
peerstate.as_ref().unwrap().prefer_encrypt
|
||||
@@ -387,6 +383,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
sendfile <file> [<text>]\n\
|
||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||
sendsyncmsg\n\
|
||||
sendupdate <msg-id> <json status update>\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
@@ -429,7 +426,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
joinqr <qr-content>\n\
|
||||
setqr <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
event <event-id to test>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
clear -- clear screen\n\
|
||||
@@ -471,20 +467,32 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"export-backup" => {
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(&context, ImexMode::ExportBackup, dir.as_ref()).await?;
|
||||
imex(
|
||||
&context,
|
||||
ImexMode::ExportBackup,
|
||||
dir.as_ref(),
|
||||
Some(arg2.to_string()),
|
||||
)
|
||||
.await?;
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
}
|
||||
"import-backup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
|
||||
imex(&context, ImexMode::ImportBackup, arg1.as_ref()).await?;
|
||||
imex(
|
||||
&context,
|
||||
ImexMode::ImportBackup,
|
||||
arg1.as_ref(),
|
||||
Some(arg2.to_string()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"export-keys" => {
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref()).await?;
|
||||
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref(), None).await?;
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
}
|
||||
"import-keys" => {
|
||||
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref()).await?;
|
||||
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
|
||||
}
|
||||
"export-setup" => {
|
||||
let setup_code = create_setup_code(&context);
|
||||
@@ -563,7 +571,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
|
||||
for i in (0..cnt).rev() {
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
||||
println!(
|
||||
"{}#{}: {} [{} fresh] {}{}{}{}",
|
||||
chat_prefix(&chat),
|
||||
@@ -706,7 +714,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"createchat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id: u32 = arg1.parse()?;
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
let chat_id = ChatId::create_for_contact(&context, contact_id).await?;
|
||||
|
||||
println!("Single#{} created successfully.", chat_id,);
|
||||
@@ -734,7 +742,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected");
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
|
||||
let contact_id_0: u32 = arg1.parse()?;
|
||||
let contact_id_0 = ContactId::new(arg1.parse()?);
|
||||
chat::add_contact_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), contact_id_0)
|
||||
.await?;
|
||||
println!("Contact added to chat.");
|
||||
@@ -742,7 +750,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"removemember" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id_1: u32 = arg1.parse()?;
|
||||
let contact_id_1 = ContactId::new(arg1.parse()?);
|
||||
chat::remove_contact_from_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
@@ -758,7 +766,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chat::set_chat_name(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
&format!("{} {}", arg1, arg2).trim(),
|
||||
format!("{} {}", arg1, arg2).trim(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -907,6 +915,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Some(msg_id) => println!("sync message sent as {}.", msg_id),
|
||||
None => println!("sync message not needed."),
|
||||
},
|
||||
"sendupdate" => {
|
||||
ensure!(
|
||||
!arg1.is_empty() && !arg2.is_empty(),
|
||||
"Arguments <msg-id> <json status update> expected"
|
||||
);
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
context
|
||||
.send_webxdc_status_update(msg_id, arg2, "this is a webxdc status update")
|
||||
.await?;
|
||||
}
|
||||
"videochat" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
@@ -1116,7 +1134,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"contactinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
|
||||
let contact_id: u32 = arg1.parse()?;
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
let contact = Contact::get_by_id(&context, contact_id).await?;
|
||||
let name_n_addr = contact.get_name_n_addr();
|
||||
|
||||
@@ -1142,7 +1160,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
if 0 != i {
|
||||
res += ", ";
|
||||
}
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
||||
res += &format!("{}#{}", chat_prefix(&chat), chat.get_id());
|
||||
}
|
||||
}
|
||||
@@ -1151,16 +1169,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"delcontact" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
Contact::delete(&context, arg1.parse()?).await?;
|
||||
Contact::delete(&context, ContactId::new(arg1.parse()?)).await?;
|
||||
}
|
||||
"block" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id = arg1.parse()?;
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
Contact::block(&context, contact_id).await?;
|
||||
}
|
||||
"unblock" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id = arg1.parse()?;
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
Contact::unblock(&context, contact_id).await?;
|
||||
}
|
||||
"listblocked" => {
|
||||
@@ -1201,17 +1219,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: implement this again, unclear how to match this through though, without writing a parser.
|
||||
// "event" => {
|
||||
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
|
||||
// let event = arg1.parse()?;
|
||||
// let event = EventType::from_u32(event).ok_or(format_err!("EventType::from_u32({})", event))?;
|
||||
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
|
||||
// println!(
|
||||
// "Sending event {:?}({}), received value {}.",
|
||||
// event, event as usize, r,
|
||||
// );
|
||||
// }
|
||||
"fileinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ const DB_COMMANDS: [&str; 10] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 35] = [
|
||||
const CHAT_COMMANDS: [&str; 36] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
@@ -191,6 +191,7 @@ const CHAT_COMMANDS: [&str; 35] = [
|
||||
"sendfile",
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"sendupdate",
|
||||
"videochat",
|
||||
"draft",
|
||||
"listmedia",
|
||||
@@ -226,13 +227,12 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 12] = [
|
||||
const MISC_COMMANDS: [&str; 11] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
"event",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
"exit",
|
||||
@@ -415,7 +415,7 @@ async fn handle_cmd(
|
||||
}
|
||||
"getqr" | "getbadqr" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
@@ -432,7 +432,7 @@ async fn handle_cmd(
|
||||
}
|
||||
"getqrsvg" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
match get_securejoin_qr_svg(&ctx, group).await {
|
||||
Ok(svg) => {
|
||||
|
||||
@@ -179,6 +179,12 @@ class Account(object):
|
||||
"""
|
||||
return True if lib.dc_is_configured(self._dc_context) else False
|
||||
|
||||
def is_open(self) -> bool:
|
||||
"""Determine if account is open
|
||||
|
||||
:returns True if account is open."""
|
||||
return True if lib.dc_context_is_open(self._dc_context) else False
|
||||
|
||||
def set_avatar(self, img_path: Optional[str]) -> None:
|
||||
"""Set self avatar.
|
||||
|
||||
|
||||
@@ -224,9 +224,7 @@ class DirectImap:
|
||||
""" (blocking) wait for next idle message from server. """
|
||||
assert self._idling
|
||||
self.account.log("imap-direct: calling idle_check")
|
||||
res = self.conn.idle_check(timeout=30)
|
||||
if len(res) == 0:
|
||||
raise TimeoutError
|
||||
res = self.conn.idle_check()
|
||||
if terminate:
|
||||
self.idle_done()
|
||||
self.account.log("imap-direct: idle_check returned {!r}".format(res))
|
||||
|
||||
@@ -241,7 +241,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
def make_account(self, path, logid, quiet=False):
|
||||
ac = Account(path, logging=self._logging)
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
ac._evtracker.set_timeout(30)
|
||||
ac.addr = ac.get_self_contact().addr
|
||||
ac.set_config("displayname", logid)
|
||||
if not quiet:
|
||||
@@ -483,7 +482,7 @@ class BotProcess:
|
||||
def kill(self) -> None:
|
||||
self.popen.kill()
|
||||
|
||||
def wait(self, timeout=30) -> None:
|
||||
def wait(self, timeout=None) -> None:
|
||||
self.popen.wait(timeout=timeout)
|
||||
|
||||
def fnmatch_lines(self, pattern_lines):
|
||||
@@ -492,7 +491,7 @@ class BotProcess:
|
||||
print("+++FNMATCH:", next_pattern)
|
||||
ignored = []
|
||||
while 1:
|
||||
line = self.stdout_queue.get(timeout=15)
|
||||
line = self.stdout_queue.get()
|
||||
if line is None:
|
||||
if ignored:
|
||||
print("BOT stdout terminated after these lines")
|
||||
|
||||
@@ -90,11 +90,11 @@ class ConfigureTracker:
|
||||
if data1 is None or evdata == data1:
|
||||
break
|
||||
|
||||
def wait_finish(self):
|
||||
def wait_finish(self, timeout=None):
|
||||
""" wait until configure is completed.
|
||||
|
||||
Raise Exception if Configure failed
|
||||
"""
|
||||
if not self._configure_events.get():
|
||||
if not self._configure_events.get(timeout=timeout):
|
||||
content = "\n".join(map(str, self._ffi_events))
|
||||
raise ConfigureFailed(content)
|
||||
|
||||
@@ -41,8 +41,8 @@ class TestOfflineAccountBasic:
|
||||
def test_wrong_db(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
p.write("123")
|
||||
with pytest.raises(ValueError):
|
||||
Account(p.strpath)
|
||||
account = Account(p.strpath)
|
||||
assert not account.is_open()
|
||||
|
||||
def test_os_name(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
@@ -652,8 +652,6 @@ class TestOnlineAccount:
|
||||
pre_generated_key=False,
|
||||
config={"key_gen_type": str(const.DC_KEY_GEN_ED25519)}
|
||||
)
|
||||
# rsa key gen can be slow especially on CI, adjust timeout
|
||||
ac1._evtracker.set_timeout(240)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
@@ -737,7 +735,6 @@ class TestOnlineAccount:
|
||||
# make sure we are not sending message to ourselves
|
||||
assert self_addr not in ev.data2
|
||||
assert other_addr in ev.data2
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_DELETED_BLOB_FILE")
|
||||
|
||||
lp.sec("ac1: setting bcc_self=1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
@@ -753,7 +750,6 @@ class TestOnlineAccount:
|
||||
# now make sure we are sending message to ourselves too
|
||||
assert self_addr in ev.data2
|
||||
assert other_addr in ev.data2
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_DELETED_BLOB_FILE")
|
||||
assert ac1.direct_imap.idle_wait_for_seen()
|
||||
|
||||
# Second client receives only second message, but not the first
|
||||
@@ -894,11 +890,11 @@ class TestOnlineAccount:
|
||||
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message1")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
chat.send_text("message2")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
chat.send_text("message3")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
def test_forward_messages(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
@@ -1071,6 +1067,62 @@ class TestOnlineAccount:
|
||||
is_seen = b'\\Seen' in flags
|
||||
assert is_seen
|
||||
|
||||
def test_multidevice_sync_seen(self, acfactory, lp):
|
||||
"""Test that message marked as seen on one device is marked as seen on another."""
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
ac1_clone = acfactory.clone_online_account(ac1)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1_chat = ac1.create_chat(ac2)
|
||||
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
|
||||
lp.sec("Send a message from ac2 to ac1 and check that it's 'fresh'")
|
||||
ac2_chat.send_text("Hi")
|
||||
ac1_message = ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
|
||||
assert ac1_chat.count_fresh_messages() == 1
|
||||
assert ac1_clone_chat.count_fresh_messages() == 1
|
||||
assert ac1_message.is_in_fresh
|
||||
assert ac1_clone_message.is_in_fresh
|
||||
|
||||
lp.sec("ac1 marks message as seen on the first device")
|
||||
ac1.mark_seen_messages([ac1_message])
|
||||
assert ac1_message.is_in_seen
|
||||
|
||||
lp.sec("ac1 clone detects that message is marked as seen")
|
||||
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
assert ev.data1 == ac1_clone_chat.id
|
||||
assert ac1_clone_message.is_in_seen
|
||||
|
||||
lp.sec("Send an ephemeral message from ac2 to ac1")
|
||||
ac2_chat.set_ephemeral_timer(60)
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
ac1_clone._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac2_chat.send_text("Foobar")
|
||||
ac1_message = ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
|
||||
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
|
||||
assert "Expires: " not in ac1_clone_message.get_message_info()
|
||||
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
|
||||
assert "Expires: " not in ac1_clone_message.get_message_info()
|
||||
|
||||
ac1.mark_seen_messages([ac1_message])
|
||||
assert ac1_message.is_in_seen
|
||||
assert "Expires: " in ac1_message.get_message_info()
|
||||
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
assert ev.data1 == ac1_clone_chat.id
|
||||
assert ac1_clone_message.is_in_seen
|
||||
# Test that the timer is started on the second device after synchronizing the seen status.
|
||||
assert "Expires: " in ac1_clone_message.get_message_info()
|
||||
|
||||
def test_message_override_sender_name(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -1370,11 +1422,13 @@ class TestOnlineAccount:
|
||||
assert not device_chat.can_send()
|
||||
assert device_chat.get_draft() is None
|
||||
|
||||
def test_dont_show_emails_in_draft_folder(self, acfactory, lp):
|
||||
def test_dont_show_emails(self, acfactory, lp):
|
||||
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
|
||||
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
|
||||
|
||||
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
|
||||
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown.
|
||||
|
||||
Also, test that unknown emails in the Spam folder are not shown."""
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac1.set_config("show_emails", "2")
|
||||
ac1.create_contact("alice@example.org").create_chat()
|
||||
@@ -1382,6 +1436,7 @@ class TestOnlineAccount:
|
||||
acfactory.wait_configure(ac1)
|
||||
ac1.direct_imap.create_folder("Drafts")
|
||||
ac1.direct_imap.create_folder("Sent")
|
||||
ac1.direct_imap.create_folder("Spam")
|
||||
|
||||
acfactory.wait_configure_and_start_io()
|
||||
# Wait until each folder was selected once and we are IDLEing again:
|
||||
@@ -1406,6 +1461,15 @@ class TestOnlineAccount:
|
||||
|
||||
message in Sent
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
ac1.direct_imap.append("Spam", """
|
||||
From: unknown.address@junk.org
|
||||
Subject: subj
|
||||
To: {}
|
||||
Message-ID: <spam.message@junk.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Unknown message in Spam
|
||||
""".format(ac1.get_config("configured_addr")))
|
||||
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
lp.sec("All prepared, now let DC find the message")
|
||||
@@ -1419,6 +1483,10 @@ class TestOnlineAccount:
|
||||
assert msg.text == "subj – message in Sent"
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
|
||||
ac1.direct_imap.select_folder("Spam")
|
||||
assert ac1.direct_imap.get_uid_by_message_id("spam.message@junk.org")
|
||||
|
||||
ac1.stop_io()
|
||||
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
|
||||
ac1.direct_imap.select_folder("Drafts")
|
||||
@@ -1456,7 +1524,7 @@ class TestOnlineAccount:
|
||||
ac1_clone.create_chat(ac2).send_text("Hi back")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
|
||||
assert ev.data1 == first_msg_id.id
|
||||
assert ev.data1 == first_msg_id.chat.id
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 0
|
||||
assert len(list(ac1.get_fresh_messages())) == 0
|
||||
|
||||
@@ -1804,7 +1872,6 @@ class TestOnlineAccount:
|
||||
lp.sec("trigger ac setup message and return setupcode")
|
||||
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
|
||||
setup_code = ac1.initiate_key_transfer()
|
||||
ac2._evtracker.set_timeout(30)
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg.is_setup_message()
|
||||
@@ -1821,7 +1888,6 @@ class TestOnlineAccount:
|
||||
def test_ac_setup_message_twice(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.clone_online_account(ac1)
|
||||
ac2._evtracker.set_timeout(30)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
|
||||
lp.sec("trigger ac setup message but ignore")
|
||||
@@ -1995,7 +2061,7 @@ class TestOnlineAccount:
|
||||
|
||||
lp.sec("ac1: send a message to group chat to promote the group")
|
||||
chat.send_text("afterwards promoted")
|
||||
ev = in_list.get(timeout=10)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
assert chat.is_promoted()
|
||||
assert sorted(x.addr for x in chat.get_contacts()) == \
|
||||
@@ -2005,29 +2071,29 @@ class TestOnlineAccount:
|
||||
# note that if the above create_chat() would not
|
||||
# happen we would not receive a proper member_added event
|
||||
contact2 = chat.add_contact("devnull@testrun.org")
|
||||
ev = in_list.get(timeout=10)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get(timeout=10)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get(timeout=10)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "added"
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
assert ev.contact.addr == "devnull@testrun.org"
|
||||
|
||||
lp.sec("ac1: remove address2")
|
||||
chat.remove_contact(contact2)
|
||||
ev = in_list.get(timeout=10)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get(timeout=10)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "removed"
|
||||
assert ev.contact.addr == contact2.addr
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
|
||||
lp.sec("ac1: remove ac2 contact from chat")
|
||||
chat.remove_contact(ac2)
|
||||
ev = in_list.get(timeout=10)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get(timeout=10)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "removed"
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
|
||||
@@ -2474,8 +2540,7 @@ class TestOnlineAccount:
|
||||
lp.sec("ac2: deleting all messages except third")
|
||||
assert len(to_delete) == len(texts) - 1
|
||||
ac2.delete_messages(to_delete)
|
||||
for msg in to_delete:
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
|
||||
ac2._evtracker.get_info_contains("close/expunge succeeded")
|
||||
|
||||
@@ -2652,7 +2717,13 @@ class TestOnlineAccount:
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
ac1.direct_imap.idle_start()
|
||||
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
|
||||
ac1.direct_imap.idle_check(terminate=True)
|
||||
while True:
|
||||
if len(ac1.direct_imap.idle_check(terminate=True)) > 1:
|
||||
# If length is 1, it's [(b'OK', b'Still here')]
|
||||
# Could happen on very slow network.
|
||||
#
|
||||
# More is usually [(1, b'EXISTS'), (1, b'RECENT')]
|
||||
break
|
||||
ac1.direct_imap.conn.move(["*"], folder) # "*" means "biggest UID in mailbox"
|
||||
|
||||
lp.sec("Everything prepared, now see if DeltaChat finds the message (" + variant + ")")
|
||||
@@ -2685,7 +2756,6 @@ class TestOnlineAccount:
|
||||
assert ac.get_config("configured_mvbox_folder")
|
||||
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
ac1.set_config("sentbox_move", "1")
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
|
||||
acfactory.wait_configure(ac1)
|
||||
@@ -2705,7 +2775,7 @@ class TestOnlineAccount:
|
||||
if mvbox_move:
|
||||
ac1.direct_imap.select_config_folder("mvbox")
|
||||
else:
|
||||
ac1.direct_imap.select_config_folder("sentbox")
|
||||
ac1.direct_imap.select_folder("INBOX")
|
||||
ac1.direct_imap.idle_start()
|
||||
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
|
||||
@@ -36,7 +36,8 @@ def test_wrong_db(tmpdir):
|
||||
# write an invalid database file
|
||||
p.write("x123" * 10)
|
||||
|
||||
assert ffi.NULL == lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
|
||||
context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
|
||||
assert not lib.dc_context_is_open(context)
|
||||
|
||||
|
||||
def test_empty_blobdir(tmpdir):
|
||||
|
||||
@@ -78,8 +78,8 @@ commands =
|
||||
addopts = -v -ra --strict-markers
|
||||
norecursedirs = .tox
|
||||
xfail_strict=true
|
||||
timeout = 90
|
||||
timeout_method = thread
|
||||
timeout = 150
|
||||
timeout_func_only = True
|
||||
markers =
|
||||
ignored: ignore this test in default test runs, use --ignored to run.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.54.0
|
||||
1.59.0
|
||||
|
||||
@@ -8,7 +8,7 @@ set -e -x
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.54.0
|
||||
RUST_VERSION=1.59.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
|
||||
@@ -8,7 +8,7 @@ set -e -x
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.54.0
|
||||
RUST_VERSION=1.59.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
|
||||
@@ -56,8 +56,13 @@ impl Accounts {
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists().await, "accounts.toml does not exist");
|
||||
|
||||
let config = Config::from_file(config_file).await?;
|
||||
let accounts = config.load_accounts().await?;
|
||||
let config = Config::from_file(config_file)
|
||||
.await
|
||||
.context("failed to load accounts config")?;
|
||||
let accounts = config
|
||||
.load_accounts()
|
||||
.await
|
||||
.context("failed to load accounts")?;
|
||||
|
||||
let emitter = EventEmitter::new();
|
||||
|
||||
@@ -66,7 +71,9 @@ impl Accounts {
|
||||
emitter.sender.send(events.get_emitter()).await?;
|
||||
|
||||
for account in accounts.values() {
|
||||
emitter.add_account(account).await?;
|
||||
emitter.add_account(account).await.with_context(|| {
|
||||
format!("failed to add account {} to event emitter", account.id)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
@@ -104,7 +111,9 @@ impl Accounts {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a new account.
|
||||
/// Add a new account and opens it.
|
||||
///
|
||||
/// Returns account ID.
|
||||
pub async fn add_account(&mut self) -> Result<u32> {
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
|
||||
@@ -115,6 +124,17 @@ impl Accounts {
|
||||
Ok(account_config.id)
|
||||
}
|
||||
|
||||
/// Adds a new closed account.
|
||||
pub async fn add_closed_account(&mut self) -> Result<u32> {
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
|
||||
let ctx = Context::new_closed(account_config.dbfile().into(), account_config.id).await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
|
||||
Ok(account_config.id)
|
||||
}
|
||||
|
||||
/// Remove an account.
|
||||
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
|
||||
let ctx = self.accounts.remove(&id);
|
||||
@@ -182,7 +202,7 @@ impl Accounts {
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
let ctx = Context::with_blobdir(new_dbfile, new_blobdir, account_config.id).await?;
|
||||
let ctx = Context::new(new_dbfile, account_config.id).await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
Ok(account_config.id)
|
||||
@@ -383,7 +403,15 @@ impl Config {
|
||||
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
|
||||
let mut accounts = BTreeMap::new();
|
||||
for account_config in &self.inner.accounts {
|
||||
let ctx = Context::new(account_config.dbfile().into(), account_config.id).await?;
|
||||
let ctx = Context::new(account_config.dbfile().into(), account_config.id)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to create context from file {:?}",
|
||||
account_config.dbfile()
|
||||
)
|
||||
})?;
|
||||
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
|
||||
@@ -408,8 +436,13 @@ impl Config {
|
||||
|
||||
self.sync().await?;
|
||||
|
||||
self.select_account(id).await.expect("just added");
|
||||
let cfg = self.get_account(id).await.expect("just added");
|
||||
self.select_account(id)
|
||||
.await
|
||||
.context("failed to select just added account")?;
|
||||
let cfg = self
|
||||
.get_account(id)
|
||||
.await
|
||||
.context("failed to get just added account")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
@@ -701,4 +734,49 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_encrypted_account() -> Result<()> {
|
||||
let dir = tempfile::tempdir().context("failed to create tempdir")?;
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new(p.clone())
|
||||
.await
|
||||
.context("failed to create accounts manager")?;
|
||||
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
let account_id = accounts
|
||||
.add_closed_account()
|
||||
.await
|
||||
.context("failed to add closed account")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.await
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.id, account_id);
|
||||
let passphrase_set_success = account
|
||||
.open("foobar".to_string())
|
||||
.await
|
||||
.context("failed to set passphrase")?;
|
||||
assert!(passphrase_set_success);
|
||||
drop(accounts);
|
||||
|
||||
let accounts = Accounts::new(p.clone())
|
||||
.await
|
||||
.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);
|
||||
|
||||
// Try wrong passphrase.
|
||||
assert_eq!(account.open("barfoo".to_string()).await?, false);
|
||||
assert_eq!(account.open("".to_string()).await?, false);
|
||||
|
||||
assert_eq!(account.open("foobar".to_string()).await?, true);
|
||||
assert_eq!(account.is_open().await, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
|
||||
|
||||
use anyhow::{bail, format_err, Error, Result};
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, str};
|
||||
@@ -139,15 +139,14 @@ impl str::FromStr for Aheader {
|
||||
};
|
||||
let public_key: SignedPublicKey = attributes
|
||||
.remove("keydata")
|
||||
.ok_or_else(|| format_err!("keydata attribute is not found"))
|
||||
.context("keydata attribute is not found")
|
||||
.and_then(|raw| {
|
||||
SignedPublicKey::from_base64(&raw)
|
||||
.map_err(|_| format_err!("Autocrypt key cannot be decoded"))
|
||||
SignedPublicKey::from_base64(&raw).context("autocrypt key cannot be decoded")
|
||||
})
|
||||
.and_then(|key| {
|
||||
key.verify()
|
||||
.and(Ok(key))
|
||||
.map_err(|_| format_err!("Autocrypt key cannot be verified"))
|
||||
.context("autocrypt key cannot be verified")
|
||||
})?;
|
||||
|
||||
let prefer_encrypt = attributes
|
||||
|
||||
82
src/blob.rs
82
src/blob.rs
@@ -3,28 +3,26 @@
|
||||
use core::cmp::max;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{fs, io};
|
||||
|
||||
use anyhow::format_err;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Error;
|
||||
use image::DynamicImage;
|
||||
use image::GenericImageView;
|
||||
use image::ImageFormat;
|
||||
use anyhow::{format_err, Context as _, Error};
|
||||
use image::{DynamicImage, ImageFormat};
|
||||
use num_traits::FromPrimitive;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
MediaQuality, Viewtype, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE,
|
||||
WORSE_IMAGE_SIZE,
|
||||
MediaQuality, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE, WORSE_IMAGE_SIZE,
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::message;
|
||||
use crate::message::Viewtype;
|
||||
|
||||
/// Represents a file in the blob directory.
|
||||
///
|
||||
@@ -63,7 +61,7 @@ impl<'a> BlobObject<'a> {
|
||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||
let blobdir = context.get_blobdir();
|
||||
let (stem, ext) = BlobObject::sanitise_name(suggested_name);
|
||||
let (name, mut file) = BlobObject::create_new_file(blobdir, &stem, &ext).await?;
|
||||
let (name, mut file) = BlobObject::create_new_file(context, blobdir, &stem, &ext).await?;
|
||||
file.write_all(data)
|
||||
.await
|
||||
.map_err(|err| BlobError::WriteFailure {
|
||||
@@ -87,13 +85,16 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
// Creates a new file, returning a tuple of the name and the handle.
|
||||
async fn create_new_file(
|
||||
context: &Context,
|
||||
dir: &Path,
|
||||
stem: &str,
|
||||
ext: &str,
|
||||
) -> Result<(String, fs::File), BlobError> {
|
||||
let max_attempt = 15;
|
||||
const MAX_ATTEMPT: u32 = 16;
|
||||
let mut attempt = 0;
|
||||
let mut name = format!("{}{}", stem, ext);
|
||||
for attempt in 0..max_attempt {
|
||||
loop {
|
||||
attempt += 1;
|
||||
let path = dir.join(&name);
|
||||
match fs::OpenOptions::new()
|
||||
.create_new(true)
|
||||
@@ -103,24 +104,20 @@ impl<'a> BlobObject<'a> {
|
||||
{
|
||||
Ok(file) => return Ok((name, file)),
|
||||
Err(err) => {
|
||||
if attempt == max_attempt {
|
||||
if attempt >= MAX_ATTEMPT {
|
||||
return Err(BlobError::CreateFailure {
|
||||
blobdir: dir.to_path_buf(),
|
||||
blobname: name,
|
||||
cause: err,
|
||||
});
|
||||
} else if attempt == 1 && !dir.exists().await {
|
||||
fs::create_dir_all(dir).await.ok_or_log(context);
|
||||
} else {
|
||||
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// This is supposed to be unreachable, but the compiler doesn't know.
|
||||
Err(BlobError::CreateFailure {
|
||||
blobdir: dir.to_path_buf(),
|
||||
blobname: name,
|
||||
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new blob object with unique name by copying an existing file.
|
||||
@@ -149,7 +146,7 @@ impl<'a> BlobObject<'a> {
|
||||
})?;
|
||||
let (stem, ext) = BlobObject::sanitise_name(&src.to_string_lossy());
|
||||
let (name, mut dst_file) =
|
||||
BlobObject::create_new_file(context.get_blobdir(), &stem, &ext).await?;
|
||||
BlobObject::create_new_file(context, context.get_blobdir(), &stem, &ext).await?;
|
||||
let name_for_err = name.clone();
|
||||
if let Err(err) = io::copy(&mut src_file, &mut dst_file).await {
|
||||
{
|
||||
@@ -292,7 +289,7 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
/// Returns the filename of the blob.
|
||||
pub fn as_file_name(&self) -> &str {
|
||||
self.name.rsplitn(2, '/').next().unwrap()
|
||||
self.name.rsplit('/').next().unwrap()
|
||||
}
|
||||
|
||||
/// The path relative in the blob directory.
|
||||
@@ -305,7 +302,7 @@ impl<'a> BlobObject<'a> {
|
||||
/// If a blob's filename has an extension, it is always guaranteed
|
||||
/// to be lowercase.
|
||||
pub fn suffix(&self) -> Option<&str> {
|
||||
let ext = self.name.rsplitn(2, '.').next();
|
||||
let ext = self.name.rsplit('.').next();
|
||||
if ext == Some(&self.name) {
|
||||
None
|
||||
} else {
|
||||
@@ -348,13 +345,30 @@ impl<'a> BlobObject<'a> {
|
||||
};
|
||||
|
||||
let clean = sanitize_filename::sanitize_with_options(name, opts);
|
||||
// Let's take the tricky filename
|
||||
// "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example.
|
||||
// Split it into "file" and "with_lots_of_characters_behind_point_and_double_ending.tar.gz":
|
||||
let mut iter = clean.splitn(2, '.');
|
||||
|
||||
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
|
||||
let ext: String = iter.next().unwrap_or_default().chars().take(32).collect();
|
||||
// stem == "file"
|
||||
|
||||
let ext_chars = iter.next().unwrap_or_default().chars();
|
||||
let ext: String = ext_chars
|
||||
.rev()
|
||||
.take(32)
|
||||
.collect::<Vec<_>>()
|
||||
.iter()
|
||||
.rev()
|
||||
.collect();
|
||||
// ext == "d_point_and_double_ending.tar.gz"
|
||||
|
||||
if ext.is_empty() {
|
||||
(stem, "".to_string())
|
||||
} else {
|
||||
(stem, format!(".{}", ext).to_lowercase())
|
||||
// Return ("file", ".d_point_and_double_ending.tar.gz")
|
||||
// which is not perfect but acceptable.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,7 +463,8 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
|
||||
encoded.clear();
|
||||
img.write_to(encoded, image::ImageFormat::Jpeg)?;
|
||||
let mut buf = Cursor::new(encoded);
|
||||
img.write_to(&mut buf, image::ImageFormat::Jpeg)?;
|
||||
Ok(())
|
||||
}
|
||||
fn encoded_img_exceeds_bytes(
|
||||
@@ -619,16 +634,14 @@ pub enum BlobError {
|
||||
mod tests {
|
||||
use fs::File;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::{
|
||||
chat,
|
||||
message::Message,
|
||||
test_utils::{self, TestContext},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use image::Pixel;
|
||||
use image::{GenericImageView, Pixel};
|
||||
|
||||
use crate::chat::{self, create_group_chat, ProtectionStatus};
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create() {
|
||||
@@ -960,10 +973,11 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let mut bytes = vec![];
|
||||
let mut buf = Cursor::new(vec![]);
|
||||
img_rotated
|
||||
.write_to(&mut bytes, image::ImageFormat::Jpeg)
|
||||
.write_to(&mut buf, image::ImageFormat::Jpeg)
|
||||
.unwrap();
|
||||
let bytes = buf.into_inner();
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Some("0"),
|
||||
&bytes,
|
||||
|
||||
517
src/chat.rs
517
src/chat.rs
@@ -5,7 +5,7 @@ use std::convert::{TryFrom, TryInto};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -15,12 +15,11 @@ use crate::blob::{BlobError, BlobObject};
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
Blocked, Chattype, Viewtype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
|
||||
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_INFO,
|
||||
DC_CONTACT_ID_LAST_SPECIAL, DC_CONTACT_ID_SELF, DC_GCM_ADDDAYMARKER, DC_GCM_INFO_ONLY,
|
||||
DC_RESEND_USER_AVATAR_DAYS,
|
||||
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
|
||||
DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_INFO, DC_CONTACT_ID_LAST_SPECIAL,
|
||||
DC_CONTACT_ID_SELF, DC_GCM_ADDDAYMARKER, DC_GCM_INFO_ONLY, DC_RESEND_USER_AVATAR_DAYS,
|
||||
};
|
||||
use crate::contact::{addr_cmp, Contact, Origin, VerifiedStatus};
|
||||
use crate::contact::{addr_cmp, Contact, ContactId, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::dc_receive_imf::ReceivedMsg;
|
||||
use crate::dc_tools::{
|
||||
@@ -32,11 +31,16 @@ use crate::ephemeral::{delete_expired_messages, schedule_ephemeral_task, Timer a
|
||||
use crate::events::EventType;
|
||||
use crate::html::new_html_mimepart;
|
||||
use crate::job::{self, Action};
|
||||
use crate::message::{self, Message, MessageState, MsgId};
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::stock_str;
|
||||
use crate::webxdc::WEBXDC_SUFFIX;
|
||||
use crate::{location, sql};
|
||||
|
||||
/// An chat item, such as a message or a marker.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
@@ -148,7 +152,10 @@ impl ChatId {
|
||||
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
|
||||
///
|
||||
/// If it does not exist, `None` is returned.
|
||||
pub async fn lookup_by_contact(context: &Context, contact_id: u32) -> Result<Option<Self>> {
|
||||
pub async fn lookup_by_contact(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
) -> Result<Option<Self>> {
|
||||
ChatIdBlocked::lookup_by_contact(context, contact_id)
|
||||
.await
|
||||
.map(|lookup| lookup.map(|chat| chat.id))
|
||||
@@ -161,7 +168,7 @@ impl ChatId {
|
||||
/// This is an internal API, if **a user action** needs to get a chat
|
||||
/// [`ChatId::create_for_contact`] should be used as this also scales up the
|
||||
/// [`Contact`]'s origin.
|
||||
pub async fn get_for_contact(context: &Context, contact_id: u32) -> Result<Self> {
|
||||
pub async fn get_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
|
||||
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
|
||||
.await
|
||||
.map(|chat| chat.id)
|
||||
@@ -170,8 +177,8 @@ impl ChatId {
|
||||
/// Returns the unblocked 1:1 chat with `contact_id`.
|
||||
///
|
||||
/// This should be used when **a user action** creates a chat 1:1, it ensures the chat
|
||||
/// exists and is unblocked and scales the [`Contact`]'s origin.
|
||||
pub async fn create_for_contact(context: &Context, contact_id: u32) -> Result<Self> {
|
||||
/// exists, is unblocked and scales the [`Contact`]'s origin.
|
||||
pub async fn create_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
|
||||
ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await
|
||||
}
|
||||
|
||||
@@ -180,7 +187,7 @@ impl ChatId {
|
||||
/// `create_blocked` won't block already unblocked chats again.
|
||||
pub(crate) async fn create_for_contact_with_blocked(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
create_blocked: Blocked,
|
||||
) -> Result<Self> {
|
||||
let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
|
||||
@@ -415,7 +422,7 @@ impl ChatId {
|
||||
context: &Context,
|
||||
protect: ProtectionStatus,
|
||||
promote: bool,
|
||||
from_id: u32,
|
||||
from_id: ContactId,
|
||||
) -> Result<()> {
|
||||
let msg_text = context.stock_protection_msg(protect, from_id).await;
|
||||
let cmd = match protect {
|
||||
@@ -438,6 +445,7 @@ impl ChatId {
|
||||
&msg_text,
|
||||
cmd,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -468,22 +476,21 @@ impl ChatId {
|
||||
self
|
||||
);
|
||||
|
||||
if visibility == ChatVisibility::Archived {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
|
||||
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET archived=? WHERE id=?;",
|
||||
paramsv![visibility, self],
|
||||
)
|
||||
.transaction(move |transaction| {
|
||||
if visibility == ChatVisibility::Archived {
|
||||
transaction.execute(
|
||||
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
|
||||
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
|
||||
)?;
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE chats SET archived=? WHERE id=?;",
|
||||
paramsv![visibility, self],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
@@ -620,7 +627,10 @@ impl ChatId {
|
||||
/// Returns `true`, if message was deleted, `false` otherwise.
|
||||
async fn maybe_delete_draft(self, context: &Context) -> Result<bool> {
|
||||
match self.get_draft_msg_id(context).await? {
|
||||
Some(msg_id) => Ok(msg_id.delete_from_db(context).await.is_ok()),
|
||||
Some(msg_id) => {
|
||||
msg_id.delete_from_db(context).await?;
|
||||
Ok(true)
|
||||
}
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
@@ -640,8 +650,11 @@ impl ChatId {
|
||||
.param
|
||||
.get_blob(Param::File, context, !msg.is_increation())
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("No file stored in params"))?;
|
||||
.context("no file stored in params")?;
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
if blob.suffix() == Some(WEBXDC_SUFFIX) {
|
||||
msg.viewtype = Viewtype::Webxdc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,7 +962,7 @@ impl std::fmt::Display for ChatId {
|
||||
/// well as query for a [ChatId].
|
||||
impl rusqlite::types::ToSql for ChatId {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Integer(self.0 as i64);
|
||||
let val = rusqlite::types::Value::Integer(i64::from(self.0));
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
@@ -959,7 +972,7 @@ impl rusqlite::types::ToSql for ChatId {
|
||||
impl rusqlite::types::FromSql for ChatId {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).and_then(|val| {
|
||||
if 0 <= val && val <= std::u32::MAX as i64 {
|
||||
if 0 <= val && val <= i64::from(std::u32::MAX) {
|
||||
Ok(ChatId::new(val as u32))
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(val))
|
||||
@@ -1149,7 +1162,7 @@ impl Chat {
|
||||
/// deltachat, and the data returned is still subject to change.
|
||||
pub async fn get_info(&self, context: &Context) -> Result<ChatInfo> {
|
||||
let draft = match self.id.get_draft(context).await? {
|
||||
Some(message) => message.text.unwrap_or_else(String::new),
|
||||
Some(message) => message.text.unwrap_or_default(),
|
||||
_ => String::new(),
|
||||
};
|
||||
Ok(ChatInfo {
|
||||
@@ -1276,7 +1289,7 @@ impl Chat {
|
||||
msg.param.set_int(Param::AttachGroupImage, 1);
|
||||
self.param.remove(Param::Unpromoted);
|
||||
self.update_param(context).await?;
|
||||
// send_sync_msg() is called (usually) a moment later at Job::send_msg_to_smtp()
|
||||
// send_sync_msg() is called (usually) a moment later at send_msg_to_smtp()
|
||||
// when the group-creation message is actually sent though SMTP -
|
||||
// this makes sure, the other devices are aware of grpid that is used in the sync-message.
|
||||
context.sync_qr_code_tokens(Some(self.id)).await?;
|
||||
@@ -1617,7 +1630,11 @@ pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<String> {
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
async fn update_special_chat_name(context: &Context, contact_id: u32, name: String) -> Result<()> {
|
||||
async fn update_special_chat_name(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
name: String,
|
||||
) -> Result<()> {
|
||||
if let Some(chat_id) = ChatId::lookup_by_contact(context, contact_id).await? {
|
||||
// the `!= name` condition avoids unneeded writes
|
||||
context
|
||||
@@ -1664,9 +1681,15 @@ impl ChatIdBlocked {
|
||||
/// Searches the database for the 1:1 chat with this contact.
|
||||
///
|
||||
/// If no chat is found `None` is returned.
|
||||
pub async fn lookup_by_contact(context: &Context, contact_id: u32) -> Result<Option<Self>> {
|
||||
pub async fn lookup_by_contact(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
) -> Result<Option<Self>> {
|
||||
ensure!(context.sql.is_open().await, "Database not available");
|
||||
ensure!(contact_id > 0, "Invalid contact id requested");
|
||||
ensure!(
|
||||
contact_id > ContactId::new(0),
|
||||
"Invalid contact id requested"
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
@@ -1695,11 +1718,14 @@ impl ChatIdBlocked {
|
||||
/// state.
|
||||
pub async fn get_for_contact(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
create_blocked: Blocked,
|
||||
) -> Result<Self> {
|
||||
ensure!(context.sql.is_open().await, "Database not available");
|
||||
ensure!(contact_id > 0, "Invalid contact id requested");
|
||||
ensure!(
|
||||
contact_id > ContactId::new(0),
|
||||
"Invalid contact id requested"
|
||||
);
|
||||
|
||||
if let Some(res) = Self::lookup_by_contact(context, contact_id).await? {
|
||||
// Already exists, no need to create.
|
||||
@@ -1781,32 +1807,15 @@ pub async fn prepare_msg(context: &Context, chat_id: ChatId, msg: &mut Message)
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
pub(crate) fn msgtype_has_file(msgtype: Viewtype) -> bool {
|
||||
match msgtype {
|
||||
Viewtype::Unknown => false,
|
||||
Viewtype::Text => false,
|
||||
Viewtype::Image => true,
|
||||
Viewtype::Gif => true,
|
||||
Viewtype::Sticker => true,
|
||||
Viewtype::Audio => true,
|
||||
Viewtype::Voice => true,
|
||||
Viewtype::Video => true,
|
||||
Viewtype::File => true,
|
||||
Viewtype::VideochatInvitation => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
|
||||
// the caller should check if the message text is empty
|
||||
} else if msgtype_has_file(msg.viewtype) {
|
||||
} else if msg.viewtype.has_file() {
|
||||
let blob = msg
|
||||
.param
|
||||
.get_blob(Param::File, context, !msg.is_increation())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
format_err!("Attachment missing for message of type #{}", msg.viewtype)
|
||||
})?;
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
|
||||
if msg.viewtype == Viewtype::Image {
|
||||
if let Err(e) = blob.recode_to_image_size(context).await {
|
||||
@@ -1825,16 +1834,30 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
if let Some((better_type, better_mime)) =
|
||||
message::guess_msgtype_from_suffix(&blob.to_abs_path())
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
if !msg.param.exists(Param::MimeType) {
|
||||
msg.param.set(Param::MimeType, better_mime);
|
||||
if better_type != Viewtype::Webxdc
|
||||
|| context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
if !msg.param.exists(Param::MimeType) {
|
||||
msg.param.set(Param::MimeType, better_mime);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !msg.param.exists(Param::MimeType) {
|
||||
} else if msg.viewtype == Viewtype::Webxdc {
|
||||
context
|
||||
.ensure_sendable_webxdc_file(&blob.to_abs_path())
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !msg.param.exists(Param::MimeType) {
|
||||
if let Some((_, mime)) = message::guess_msgtype_from_suffix(&blob.to_abs_path()) {
|
||||
msg.param.set(Param::MimeType, mime);
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Attaching \"{}\" for message type #{}.",
|
||||
@@ -1890,7 +1913,7 @@ async fn prepare_msg_common(
|
||||
pub async fn is_contact_in_chat(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
) -> Result<bool> {
|
||||
// this function works for group and for normal chats, however, it is more useful
|
||||
// for group chats.
|
||||
@@ -1901,7 +1924,7 @@ pub async fn is_contact_in_chat(
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
|
||||
paramsv![chat_id, contact_id as i32],
|
||||
paramsv![chat_id, contact_id],
|
||||
)
|
||||
.await?;
|
||||
Ok(exists)
|
||||
@@ -1938,41 +1961,25 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
|
||||
|
||||
/// Tries to send a message synchronously.
|
||||
///
|
||||
/// Directly opens an smtp
|
||||
/// connection and sends the message, bypassing the job system. If this fails, it writes a send job to
|
||||
/// the database.
|
||||
/// Creates a new message in `smtp` table, then drectly opens an SMTP connection and sends the
|
||||
/// message. If this fails, the message remains in the database to be sent later.
|
||||
pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
if let Some(mut job) = prepare_send_msg(context, chat_id, msg).await? {
|
||||
if let Some(rowid) = prepare_send_msg(context, chat_id, msg).await? {
|
||||
let mut smtp = crate::smtp::Smtp::new();
|
||||
send_msg_to_smtp(context, &mut smtp, rowid)
|
||||
.await
|
||||
.context("failed to send message, queued for later sending")?;
|
||||
|
||||
let status = job.send_msg_to_smtp(context, &mut smtp).await;
|
||||
|
||||
match status {
|
||||
job::Status::Finished(Ok(_)) => {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
|
||||
Ok(msg.id)
|
||||
}
|
||||
_ => {
|
||||
job.save(context).await?;
|
||||
Err(format_err!(
|
||||
"failed to send message, queued for later sending"
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Nothing to do
|
||||
Ok(msg.id)
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
}
|
||||
Ok(msg.id)
|
||||
}
|
||||
|
||||
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
if let Some(send_job) = prepare_send_msg(context, chat_id, msg).await? {
|
||||
job::add(context, send_job).await?;
|
||||
|
||||
if prepare_send_msg(context, chat_id, msg).await?.is_some() {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
@@ -1981,16 +1988,19 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
|
||||
if msg.param.exists(Param::SetLatitude) {
|
||||
context.emit_event(EventType::LocationChanged(Some(DC_CONTACT_ID_SELF)));
|
||||
}
|
||||
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
}
|
||||
|
||||
Ok(msg.id)
|
||||
}
|
||||
|
||||
/// Returns rowid from `smtp` table.
|
||||
async fn prepare_send_msg(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
msg: &mut Message,
|
||||
) -> Result<Option<crate::job::Job>> {
|
||||
) -> Result<Option<i64>> {
|
||||
// dc_prepare_msg() leaves the message state to OutPreparing, we
|
||||
// only have to change the state to OutPending in this case.
|
||||
// Otherwise we still have to prepare the message, which will set
|
||||
@@ -2004,11 +2014,145 @@ async fn prepare_send_msg(
|
||||
chat_id.is_unset() || chat_id == msg.chat_id,
|
||||
"Inconsistent chat ID"
|
||||
);
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await;
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
|
||||
}
|
||||
let job = job::send_msg_job(context, msg.id).await?;
|
||||
let row_id = create_send_msg_job(context, msg.id).await?;
|
||||
Ok(row_id)
|
||||
}
|
||||
|
||||
Ok(job)
|
||||
/// Constructs a job for sending a message and inserts into `smtp` table.
|
||||
///
|
||||
/// Returns rowid if job was created or `None` if SMTP job is not needed, e.g. when sending to a
|
||||
/// group with only self and no BCC-to-self configured.
|
||||
///
|
||||
/// The caller has to interrupt SMTP loop or otherwise process a new row.
|
||||
async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<i64>> {
|
||||
let mut msg = Message::load_from_db(context, msg_id).await?;
|
||||
msg.try_calc_and_set_dimensions(context)
|
||||
.await
|
||||
.context("failed to calculate media dimensions")?;
|
||||
|
||||
/* create message */
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
|
||||
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
|
||||
Ok(attach_selfavatar) => attach_selfavatar,
|
||||
Err(err) => {
|
||||
warn!(context, "job: cannot get selfavatar-state: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar).await?;
|
||||
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
let from = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
// Send BCC to self if it is enabled and we are not going to
|
||||
// delete it immediately.
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
&& context.get_config_delete_server_after().await? != Some(0)
|
||||
&& !recipients
|
||||
.iter()
|
||||
.any(|x| x.to_lowercase() == lowercase_from)
|
||||
{
|
||||
recipients.push(from);
|
||||
}
|
||||
|
||||
if recipients.is_empty() {
|
||||
// may happen eg. for groups with only SELF and bcc_self disabled
|
||||
info!(
|
||||
context,
|
||||
"message {} has no recipient, skipping smtp-send", msg_id
|
||||
);
|
||||
msg_id.set_delivered(context).await?;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let rendered_msg = match mimefactory.render(context).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
|
||||
Err(err)
|
||||
}
|
||||
}?;
|
||||
|
||||
if needs_encryption && !rendered_msg.is_encrypted {
|
||||
/* unrecoverable */
|
||||
message::set_msg_failed(
|
||||
context,
|
||||
msg_id,
|
||||
Some("End-to-end-encryption unavailable unexpectedly."),
|
||||
)
|
||||
.await;
|
||||
bail!(
|
||||
"e2e encryption unavailable {} - {:?}",
|
||||
msg_id,
|
||||
needs_encryption
|
||||
);
|
||||
}
|
||||
|
||||
if rendered_msg.is_gossiped {
|
||||
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
|
||||
}
|
||||
|
||||
if 0 != rendered_msg.last_added_location_id {
|
||||
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
|
||||
error!(context, "Failed to set kml sent_timestamp: {:?}", err);
|
||||
}
|
||||
if !msg.hidden {
|
||||
if let Err(err) =
|
||||
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
|
||||
.await
|
||||
{
|
||||
error!(context, "Failed to set msg_location_id: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||
if let Err(err) = context.delete_sync_ids(sync_ids).await {
|
||||
error!(context, "Failed to delete sync ids: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if attach_selfavatar {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
|
||||
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if rendered_msg.is_encrypted && !needs_encryption {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
msg.update_param(context).await;
|
||||
}
|
||||
|
||||
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
|
||||
|
||||
let recipients = recipients.join(" ");
|
||||
|
||||
msg.subject = rendered_msg.subject.clone();
|
||||
msg.update_subject(context).await;
|
||||
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
paramsv![
|
||||
&rendered_msg.rfc724_mid,
|
||||
recipients,
|
||||
&rendered_msg.message,
|
||||
msg_id
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
Ok(Some(row_id))
|
||||
}
|
||||
|
||||
pub async fn send_text_msg(
|
||||
@@ -2082,9 +2226,12 @@ pub async fn get_chat_msgs(
|
||||
|row: &rusqlite::Row| {
|
||||
// is_info logic taken from Message.is_info()
|
||||
let params = row.get::<_, String>("param")?;
|
||||
let (from_id, to_id) = (row.get::<_, u32>("from_id")?, row.get::<_, u32>("to_id")?);
|
||||
let is_info_msg: bool = from_id == DC_CONTACT_ID_INFO as u32
|
||||
|| to_id == DC_CONTACT_ID_INFO as u32
|
||||
let (from_id, to_id) = (
|
||||
row.get::<_, ContactId>("from_id")?,
|
||||
row.get::<_, ContactId>("to_id")?,
|
||||
);
|
||||
let is_info_msg: bool = from_id == DC_CONTACT_ID_INFO
|
||||
|| to_id == DC_CONTACT_ID_INFO
|
||||
|| match Params::from_str(¶ms) {
|
||||
Ok(p) => {
|
||||
let cmd = p.get_cmd();
|
||||
@@ -2160,11 +2307,11 @@ pub async fn get_chat_msgs(
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
"SELECT (m.id+1) AS id, (m.timestamp+1) AS timestamp
|
||||
FROM msgs m
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0
|
||||
ORDER BY m.timestamp, m.id;",
|
||||
ORDER BY timestamp, id;",
|
||||
paramsv![chat_id],
|
||||
process_row,
|
||||
process_rows,
|
||||
@@ -2385,7 +2532,7 @@ pub async fn get_next_media(
|
||||
}
|
||||
|
||||
/// Returns a vector of contact IDs for given chat ID.
|
||||
pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<u32>> {
|
||||
pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
|
||||
// Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a
|
||||
// groupchat but the chats stays visible, moreover, this makes displaying lists easier)
|
||||
|
||||
@@ -2399,7 +2546,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
|
||||
WHERE cc.chat_id=?
|
||||
ORDER BY c.id=1, LOWER(c.name||c.addr), c.id;",
|
||||
paramsv![chat_id],
|
||||
|row| row.get::<_, u32>(0),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
@@ -2506,13 +2653,13 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
|
||||
pub(crate) async fn add_to_chat_contacts_table(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
paramsv![chat_id, contact_id as i32],
|
||||
paramsv![chat_id, contact_id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -2522,13 +2669,13 @@ pub(crate) async fn add_to_chat_contacts_table(
|
||||
pub(crate) async fn remove_from_chat_contacts_table(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
|
||||
paramsv![chat_id, contact_id as i32],
|
||||
paramsv![chat_id, contact_id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -2538,7 +2685,7 @@ pub(crate) async fn remove_from_chat_contacts_table(
|
||||
pub async fn add_contact_to_chat(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
) -> Result<()> {
|
||||
add_contact_to_chat_ex(context, chat_id, contact_id, false).await?;
|
||||
Ok(())
|
||||
@@ -2547,7 +2694,7 @@ pub async fn add_contact_to_chat(
|
||||
pub(crate) async fn add_contact_to_chat_ex(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
from_handshake: bool,
|
||||
) -> Result<bool> {
|
||||
ensure!(!chat_id.is_special(), "can not add member to special chats");
|
||||
@@ -2624,9 +2771,8 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
|
||||
msg.text = Some(
|
||||
stock_str::msg_add_member(context, contact.get_addr(), DC_CONTACT_ID_SELF as u32).await,
|
||||
);
|
||||
msg.text =
|
||||
Some(stock_str::msg_add_member(context, contact.get_addr(), DC_CONTACT_ID_SELF).await);
|
||||
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr());
|
||||
msg.param.set_int(Param::Arg2, from_handshake.into());
|
||||
@@ -2729,7 +2875,7 @@ pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuratio
|
||||
pub async fn remove_contact_from_chat(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!chat_id.is_special(),
|
||||
@@ -2758,15 +2904,14 @@ pub async fn remove_contact_from_chat(
|
||||
msg.viewtype = Viewtype::Text;
|
||||
if contact.id == DC_CONTACT_ID_SELF {
|
||||
set_group_explicitly_left(context, &chat.grpid).await?;
|
||||
msg.text = Some(
|
||||
stock_str::msg_group_left(context, DC_CONTACT_ID_SELF as u32).await,
|
||||
);
|
||||
msg.text =
|
||||
Some(stock_str::msg_group_left(context, DC_CONTACT_ID_SELF).await);
|
||||
} else {
|
||||
msg.text = Some(
|
||||
stock_str::msg_del_member(
|
||||
context,
|
||||
contact.get_addr(),
|
||||
DC_CONTACT_ID_SELF as u32,
|
||||
DC_CONTACT_ID_SELF,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
@@ -2859,13 +3004,8 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
|
||||
if chat.is_promoted() && !chat.is_mailing_list() && chat.typ != Chattype::Broadcast {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
msg.text = Some(
|
||||
stock_str::msg_grp_name(
|
||||
context,
|
||||
&chat.name,
|
||||
&new_name,
|
||||
DC_CONTACT_ID_SELF as u32,
|
||||
)
|
||||
.await,
|
||||
stock_str::msg_grp_name(context, &chat.name, &new_name, DC_CONTACT_ID_SELF)
|
||||
.await,
|
||||
);
|
||||
msg.param.set_cmd(SystemMessage::GroupNameChanged);
|
||||
if !chat.name.is_empty() {
|
||||
@@ -2918,7 +3058,7 @@ pub async fn set_chat_profile_image(
|
||||
if new_image.as_ref().is_empty() {
|
||||
chat.param.remove(Param::ProfileImage);
|
||||
msg.param.remove(Param::Arg);
|
||||
msg.text = Some(stock_str::msg_grp_img_deleted(context, DC_CONTACT_ID_SELF as u32).await);
|
||||
msg.text = Some(stock_str::msg_grp_img_deleted(context, DC_CONTACT_ID_SELF).await);
|
||||
} else {
|
||||
let mut image_blob = match BlobObject::from_path(context, Path::new(new_image.as_ref())) {
|
||||
Ok(blob) => Ok(blob),
|
||||
@@ -2932,7 +3072,7 @@ pub async fn set_chat_profile_image(
|
||||
image_blob.recode_to_avatar_size(context).await?;
|
||||
chat.param.set(Param::ProfileImage, image_blob.as_name());
|
||||
msg.param.set(Param::Arg, image_blob.as_name());
|
||||
msg.text = Some(stock_str::msg_grp_img_changed(context, DC_CONTACT_ID_SELF as u32).await);
|
||||
msg.text = Some(stock_str::msg_grp_img_changed(context, DC_CONTACT_ID_SELF).await);
|
||||
}
|
||||
chat.update_param(context).await?;
|
||||
if chat.is_promoted() && !chat.is_mailing_list() {
|
||||
@@ -2963,7 +3103,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.query_map(
|
||||
format!(
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
msg_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
|
||||
sql::repeat_vars(msg_ids.len())?
|
||||
),
|
||||
rusqlite::params_from_iter(msg_ids),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
@@ -2993,6 +3133,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
msg.param.remove(Param::OverrideSenderDisplayname);
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.in_reply_to = None;
|
||||
|
||||
// do not leak data as group names; a default subject is generated by mimfactory
|
||||
@@ -3024,8 +3166,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
if let Some(send_job) = job::send_msg_job(context, new_msg_id).await? {
|
||||
job::add(context, send_job).await?;
|
||||
if create_send_msg_job(context, new_msg_id).await?.is_some() {
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
}
|
||||
}
|
||||
created_chats.push(chat_id);
|
||||
@@ -3239,6 +3381,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
text: &str,
|
||||
cmd: SystemMessage,
|
||||
timestamp: i64,
|
||||
parent: Option<&Message>,
|
||||
) -> Result<MsgId> {
|
||||
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
|
||||
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
|
||||
@@ -3250,7 +3393,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
|
||||
let row_id =
|
||||
context.sql.insert(
|
||||
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,type,state,txt,rfc724_mid,ephemeral_timer, param) VALUES (?,?,?, ?,?,?, ?,?,?, ?);",
|
||||
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,type,state,txt,rfc724_mid,ephemeral_timer, param,mime_in_reply_to) VALUES (?,?,?, ?,?,?, ?,?,?, ?,?);",
|
||||
paramsv![
|
||||
chat_id,
|
||||
DC_CONTACT_ID_INFO,
|
||||
@@ -3262,6 +3405,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
rfc724_mid,
|
||||
ephemeral_timer,
|
||||
param.to_string(),
|
||||
parent.map(|msg|msg.rfc724_mid.clone()).unwrap_or_default()
|
||||
]
|
||||
).await?;
|
||||
|
||||
@@ -3277,7 +3421,15 @@ pub(crate) async fn add_info_msg(
|
||||
text: &str,
|
||||
timestamp: i64,
|
||||
) -> Result<MsgId> {
|
||||
add_info_msg_with_cmd(context, chat_id, text, SystemMessage::Unknown, timestamp).await
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
chat_id,
|
||||
text,
|
||||
SystemMessage::Unknown,
|
||||
timestamp,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -3441,6 +3593,58 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_change_quotes_on_reused_message_object() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
|
||||
let quote1 =
|
||||
Message::load_from_db(&t, send_text_msg(&t, chat_id, "quote1".to_string()).await?)
|
||||
.await?;
|
||||
let quote2 =
|
||||
Message::load_from_db(&t, send_text_msg(&t, chat_id, "quote2".to_string()).await?)
|
||||
.await?;
|
||||
|
||||
// save a draft
|
||||
let mut draft = Message::new(Viewtype::Text);
|
||||
draft.set_text(Some("draft text".to_string()));
|
||||
chat_id.set_draft(&t, Some(&mut draft)).await?;
|
||||
|
||||
let test = Message::load_from_db(&t, draft.id).await?;
|
||||
assert_eq!(test.text, Some("draft text".to_string()));
|
||||
assert!(test.quoted_text().is_none());
|
||||
assert!(test.quoted_message(&t).await?.is_none());
|
||||
|
||||
// add quote to same message object
|
||||
draft.set_quote(&t, Some("e1)).await?;
|
||||
chat_id.set_draft(&t, Some(&mut draft)).await?;
|
||||
|
||||
let test = Message::load_from_db(&t, draft.id).await?;
|
||||
assert_eq!(test.text, Some("draft text".to_string()));
|
||||
assert_eq!(test.quoted_text(), Some("quote1".to_string()));
|
||||
assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote1.id);
|
||||
|
||||
// change quote on same message object
|
||||
draft.set_text(Some("another draft text".to_string()));
|
||||
draft.set_quote(&t, Some("e2)).await?;
|
||||
chat_id.set_draft(&t, Some(&mut draft)).await?;
|
||||
|
||||
let test = Message::load_from_db(&t, draft.id).await?;
|
||||
assert_eq!(test.text, Some("another draft text".to_string()));
|
||||
assert_eq!(test.quoted_text(), Some("quote2".to_string()));
|
||||
assert_eq!(test.quoted_message(&t).await?.unwrap().id, quote2.id);
|
||||
|
||||
// remove quote on same message object
|
||||
draft.set_quote(&t, None).await?;
|
||||
chat_id.set_draft(&t, Some(&mut draft)).await?;
|
||||
|
||||
let test = Message::load_from_db(&t, draft.id).await?;
|
||||
assert_eq!(test.text, Some("another draft text".to_string()));
|
||||
assert!(test.quoted_text().is_none());
|
||||
assert!(test.quoted_message(&t).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_add_contact_to_chat_ex_add_self() {
|
||||
// Adding self to a contact should succeed, even though it's pointless.
|
||||
@@ -3696,7 +3900,7 @@ mod tests {
|
||||
async fn test_self_talk() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = &t.get_self_chat().await;
|
||||
assert_eq!(DC_CONTACT_ID_SELF, 1);
|
||||
assert_eq!(DC_CONTACT_ID_SELF, ContactId::new(1));
|
||||
assert!(!chat.id.is_special());
|
||||
assert!(chat.is_self_talk());
|
||||
assert!(chat.visibility == ChatVisibility::Normal);
|
||||
@@ -3862,7 +4066,7 @@ mod tests {
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
// after the device-chat and all messages are deleted, a re-adding should do nothing
|
||||
chats.get_chat_id(0).delete(&t).await.ok();
|
||||
chats.get_chat_id(0).unwrap().delete(&t).await.ok();
|
||||
add_device_msg(&t, Some("some-label"), Some(&mut msg))
|
||||
.await
|
||||
.ok();
|
||||
@@ -4027,7 +4231,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let mut result = Vec::new();
|
||||
for chatlist_index in 0..chatlist.len() {
|
||||
result.push(chatlist.get_chat_id(chatlist_index))
|
||||
result.push(chatlist.get_chat_id(chatlist_index).unwrap())
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -4113,7 +4317,7 @@ mod tests {
|
||||
let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_ne!(contact1, 0);
|
||||
assert_ne!(contact1, ContactId::new(0));
|
||||
|
||||
let chat_id = ChatId::create_for_contact(&context.ctx, contact1)
|
||||
.await
|
||||
@@ -4211,34 +4415,37 @@ mod tests {
|
||||
assert_eq!(msg.get_text().unwrap(), "foo info");
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::Unknown);
|
||||
assert!(msg.parent(&t).await?.is_none());
|
||||
assert!(msg.quoted_message(&t).await?.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_add_info_msg_with_cmd() {
|
||||
async fn test_add_info_msg_with_cmd() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let msg_id = add_info_msg_with_cmd(
|
||||
&t,
|
||||
chat_id,
|
||||
"foo bar info",
|
||||
SystemMessage::EphemeralTimerChanged,
|
||||
10000,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let msg = Message::load_from_db(&t, msg_id).await.unwrap();
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.get_chat_id(), chat_id);
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Text);
|
||||
assert_eq!(msg.get_text().unwrap(), "foo bar info");
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::EphemeralTimerChanged);
|
||||
assert!(msg.parent(&t).await?.is_none());
|
||||
assert!(msg.quoted_message(&t).await?.is_none());
|
||||
|
||||
let msg2 = t.get_last_msg_in(chat_id).await;
|
||||
assert_eq!(msg.get_id(), msg2.get_id());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -4315,7 +4522,7 @@ mod tests {
|
||||
|
||||
// create contact, then unblocked chat
|
||||
let contact_id = Contact::create(&ctx, "", "bob@foo.de").await.unwrap();
|
||||
assert_ne!(contact_id, 0);
|
||||
assert_ne!(contact_id, ContactId::new(0));
|
||||
let found = ChatId::lookup_by_contact(&ctx, contact_id).await.unwrap();
|
||||
assert!(found.is_none());
|
||||
|
||||
@@ -4341,10 +4548,14 @@ mod tests {
|
||||
assert_eq!(chat2.blocked, Blocked::Yes);
|
||||
|
||||
// test nonexistent contact
|
||||
let found = ChatId::lookup_by_contact(&ctx, 1234).await.unwrap();
|
||||
let found = ChatId::lookup_by_contact(&ctx, ContactId::new(1234))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(found.is_none());
|
||||
|
||||
let found = ChatIdBlocked::lookup_by_contact(&ctx, 1234).await.unwrap();
|
||||
let found = ChatIdBlocked::lookup_by_contact(&ctx, ContactId::new(1234))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(found.is_none());
|
||||
}
|
||||
|
||||
@@ -4390,7 +4601,8 @@ mod tests {
|
||||
);
|
||||
|
||||
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
|
||||
let msg = alice.pop_sent_msg().await.payload();
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
assert_eq!(msg.match_indices("Gr.").count(), 2);
|
||||
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
|
||||
assert_eq!(msg.match_indices("Gr.").count(), 1);
|
||||
@@ -4410,7 +4622,8 @@ mod tests {
|
||||
// Bob answers - simulate a normal MUA by not setting `Chat-*`-headers;
|
||||
// moreover, Bob's SMTP-server also replaces the `Message-ID:`-header
|
||||
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
|
||||
let msg = bob.pop_sent_msg().await.payload();
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
|
||||
let msg = msg.replace("Chat-", "XXXX-");
|
||||
assert_eq!(msg.match_indices("Chat-").count(), 0);
|
||||
@@ -4450,7 +4663,7 @@ mod tests {
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), chat.id);
|
||||
assert_eq!(chats.get_chat_id(0)?, chat.id);
|
||||
assert_eq!(chat.id.get_fresh_msg_cnt(&t).await?, 1);
|
||||
assert_eq!(t.get_fresh_msgs().await?.len(), 1);
|
||||
|
||||
@@ -4498,7 +4711,7 @@ mod tests {
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0);
|
||||
let chat_id = chats.get_chat_id(0).unwrap();
|
||||
assert!(Chat::load_from_db(&t, chat_id)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -4546,7 +4759,7 @@ mod tests {
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request());
|
||||
assert_eq!(dc_get_archived_cnt(&t).await?, 0);
|
||||
|
||||
@@ -4555,13 +4768,13 @@ mod tests {
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert!(chat_id.is_archived_link());
|
||||
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
|
||||
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request());
|
||||
|
||||
Ok(())
|
||||
@@ -4746,7 +4959,7 @@ mod tests {
|
||||
// Bob quotes received message and sends a reply to Alice.
|
||||
let mut reply = Message::new(Viewtype::Text);
|
||||
reply.set_text(Some("Reply".to_owned()));
|
||||
reply.set_quote(&bob, &received_msg).await?;
|
||||
reply.set_quote(&bob, Some(&received_msg)).await?;
|
||||
let sent_reply = bob.send_msg(bob_chat.id, &mut reply).await;
|
||||
alice.recv_msg(&sent_reply).await;
|
||||
let received_reply = alice.get_last_msg().await;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! # Chat list module.
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
|
||||
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
|
||||
use crate::constants::{
|
||||
@@ -8,7 +8,7 @@ use crate::constants::{
|
||||
DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT, DC_GCL_ARCHIVED_ONLY,
|
||||
DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
|
||||
};
|
||||
use crate::contact::Contact;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::ephemeral::delete_expired_messages;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
@@ -85,7 +85,7 @@ impl Chatlist {
|
||||
context: &Context,
|
||||
listflags: usize,
|
||||
query: Option<&str>,
|
||||
query_contact_id: Option<u32>,
|
||||
query_contact_id: Option<ContactId>,
|
||||
) -> Result<Self> {
|
||||
let flag_archived_only = 0 != listflags & DC_GCL_ARCHIVED_ONLY;
|
||||
let flag_for_forwarding = 0 != listflags & DC_GCL_FOR_FORWARDING;
|
||||
@@ -147,7 +147,7 @@ impl Chatlist {
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
|
||||
paramsv![MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?
|
||||
@@ -271,21 +271,23 @@ impl Chatlist {
|
||||
/// Get a single chat ID of a chatlist.
|
||||
///
|
||||
/// To get the message object from the message ID, use dc_get_chat().
|
||||
pub fn get_chat_id(&self, index: usize) -> ChatId {
|
||||
match self.ids.get(index) {
|
||||
Some((chat_id, _msg_id)) => *chat_id,
|
||||
None => ChatId::new(0),
|
||||
}
|
||||
pub fn get_chat_id(&self, index: usize) -> Result<ChatId> {
|
||||
let (chat_id, _msg_id) = self
|
||||
.ids
|
||||
.get(index)
|
||||
.context("chatlist index is out of range")?;
|
||||
Ok(*chat_id)
|
||||
}
|
||||
|
||||
/// Get a single message ID of a chatlist.
|
||||
///
|
||||
/// To get the message object from the message ID, use dc_get_msg().
|
||||
pub fn get_msg_id(&self, index: usize) -> Result<Option<MsgId>> {
|
||||
match self.ids.get(index) {
|
||||
Some((_chat_id, msg_id)) => Ok(*msg_id),
|
||||
None => bail!("Chatlist index out of range"),
|
||||
}
|
||||
let (_chat_id, msg_id) = self
|
||||
.ids
|
||||
.get(index)
|
||||
.context("chatlist index is out of range")?;
|
||||
Ok(*msg_id)
|
||||
}
|
||||
|
||||
/// Returns a summary for a given chatlist index.
|
||||
@@ -299,11 +301,10 @@ impl Chatlist {
|
||||
// This is because we may want to display drafts here or stuff as
|
||||
// "is typing".
|
||||
// Also, sth. as "No messages" would not work if the summary comes from a message.
|
||||
let (chat_id, lastmsg_id) = match self.ids.get(index) {
|
||||
Some(ids) => ids,
|
||||
None => bail!("Chatlist index out of range"),
|
||||
};
|
||||
|
||||
let (chat_id, lastmsg_id) = self
|
||||
.ids
|
||||
.get(index)
|
||||
.context("chatlist index is out of range")?;
|
||||
Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
|
||||
}
|
||||
|
||||
@@ -374,8 +375,8 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
|
||||
use crate::constants::Viewtype;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::Viewtype;
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
@@ -395,9 +396,9 @@ mod tests {
|
||||
// check that the chatlist starts with the most recent message
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 3);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id3);
|
||||
assert_eq!(chats.get_chat_id(1), chat_id2);
|
||||
assert_eq!(chats.get_chat_id(2), chat_id1);
|
||||
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id3);
|
||||
assert_eq!(chats.get_chat_id(1).unwrap(), chat_id2);
|
||||
assert_eq!(chats.get_chat_id(2).unwrap(), chat_id1);
|
||||
|
||||
// New drafts are sorted to the top
|
||||
// We have to set a draft on the other two messages, too, as
|
||||
@@ -414,7 +415,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.get_chat_id(0), chat_id2);
|
||||
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id2);
|
||||
|
||||
// check chatlist query and archive functionality
|
||||
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
|
||||
@@ -445,7 +446,7 @@ mod tests {
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert!(chats.len() == 3);
|
||||
assert!(!Chat::load_from_db(&t, chats.get_chat_id(0))
|
||||
assert!(!Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
@@ -454,7 +455,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
|
||||
assert!(Chat::load_from_db(&t, chats.get_chat_id(0))
|
||||
assert!(Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
@@ -527,7 +528,7 @@ mod tests {
|
||||
// check, the one-to-one-chat can be found using chatlist search query
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id);
|
||||
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id);
|
||||
|
||||
// change the name of the contact; this also changes the name of the one-to-one-chat
|
||||
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
|
||||
@@ -583,7 +584,7 @@ mod tests {
|
||||
// check, the one-to-one-chat can be found using chatlist search query
|
||||
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id);
|
||||
assert_eq!(chats.get_chat_id(0)?, chat_id);
|
||||
|
||||
// change the name of the contact; this also changes the name of the one-to-one-chat
|
||||
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
|
||||
@@ -594,7 +595,7 @@ mod tests {
|
||||
assert_eq!(chats.len(), 0); // email-addresses are searchable in contacts, not in chats
|
||||
let chats = Chatlist::try_load(&t, 0, Some("Bob Nickname"), None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id);
|
||||
assert_eq!(chats.get_chat_id(0)?, chat_id);
|
||||
|
||||
// revert name change, this again changes the name of the one-to-one-chat to the email-address
|
||||
let test_id = Contact::create(&t, "", "bob@example.org").await?;
|
||||
|
||||
@@ -10,11 +10,9 @@ use crate::constants::DC_VERSION_STR;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input};
|
||||
use crate::events::EventType;
|
||||
use crate::job;
|
||||
use crate::message::MsgId;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
use crate::stock_str;
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
@@ -67,14 +65,18 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
MdnsEnabled,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
#[strum(props(default = "0"))]
|
||||
SentboxWatch,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
MvboxMove,
|
||||
|
||||
/// Watch for new messages in the "Mvbox" (aka DeltaChat folder) only.
|
||||
///
|
||||
/// This will not entirely disable other folders, e.g. the spam folder will also still
|
||||
/// be watched for new messages.
|
||||
#[strum(props(default = "0"))]
|
||||
SentboxMove, // If `MvboxMove` is true, this config is ignored. Currently only used in tests.
|
||||
OnlyFetchMvbox,
|
||||
|
||||
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
|
||||
ShowEmails,
|
||||
@@ -200,7 +202,6 @@ impl Context {
|
||||
|
||||
// Default values
|
||||
match key {
|
||||
Config::Selfstatus => Ok(Some(stock_str::status_line(self).await)),
|
||||
Config::ConfiguredInboxFolder => Ok(Some("INBOX".to_owned())),
|
||||
_ => Ok(key.get_str("default").map(|s| s.to_string())),
|
||||
}
|
||||
@@ -228,6 +229,11 @@ impl Context {
|
||||
Ok(self.get_config_int(key).await? != 0)
|
||||
}
|
||||
|
||||
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::MvboxMove).await?
|
||||
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
@@ -236,7 +242,7 @@ impl Context {
|
||||
match self.get_config_int(Config::DeleteServerAfter).await? {
|
||||
0 => Ok(None),
|
||||
1 => Ok(Some(0)),
|
||||
x => Ok(Some(x as i64)),
|
||||
x => Ok(Some(i64::from(x))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +264,7 @@ impl Context {
|
||||
pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
|
||||
match self.get_config_int(Config::DeleteDeviceAfter).await? {
|
||||
0 => Ok(None),
|
||||
x => Ok(Some(x as i64)),
|
||||
x => Ok(Some(i64::from(x))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,55 +290,29 @@ impl Context {
|
||||
}
|
||||
}
|
||||
self.emit_event(EventType::SelfavatarChanged);
|
||||
Ok(())
|
||||
}
|
||||
Config::Selfstatus => {
|
||||
let def = stock_str::status_line(self).await;
|
||||
let val = if value.is_none() || value.unwrap() == def {
|
||||
None
|
||||
} else {
|
||||
value
|
||||
};
|
||||
|
||||
self.sql.set_raw_config(key, val).await?;
|
||||
Ok(())
|
||||
}
|
||||
Config::DeleteDeviceAfter => {
|
||||
let ret = self
|
||||
.sql
|
||||
.set_raw_config(key, value)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
let ret = self.sql.set_raw_config(key, value).await;
|
||||
// Force chatlist reload to delete old messages immediately.
|
||||
self.emit_event(EventType::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
});
|
||||
ret
|
||||
ret?
|
||||
}
|
||||
Config::Displayname => {
|
||||
let value = value.map(improve_single_line_input);
|
||||
self.sql.set_raw_config(key, value.as_deref()).await?;
|
||||
Ok(())
|
||||
}
|
||||
Config::DeleteServerAfter => {
|
||||
let ret = self
|
||||
.sql
|
||||
.set_raw_config(key, value)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
job::schedule_resync(self).await?;
|
||||
ret
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key, value).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
|
||||
self.set_config(key, if value { Some("1") } else { None })
|
||||
self.set_config(key, if value { Some("1") } else { Some("0") })
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -424,4 +404,17 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
|
||||
#[async_std::test]
|
||||
async fn test_set_config_bool() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// We need some config that defaults to true
|
||||
let c = Config::E2eeEnabled;
|
||||
assert_eq!(t.get_config_bool(c).await?, true);
|
||||
t.set_config_bool(c, false).await?;
|
||||
assert_eq!(t.get_config_bool(c).await?, false);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,21 +11,20 @@ use async_std::task;
|
||||
use job::Action;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{time, EmailAddress};
|
||||
use crate::imap::Imap;
|
||||
use crate::job;
|
||||
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam, Socks5Config};
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::oauth2::dc_get_oauth2_addr;
|
||||
use crate::param::Params;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::stock_str;
|
||||
use crate::{chat, e2ee, provider};
|
||||
use crate::{config::Config, dc_tools::time};
|
||||
use crate::{
|
||||
constants::{Viewtype, DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2},
|
||||
job,
|
||||
};
|
||||
use crate::{context::Context, param::Params};
|
||||
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
@@ -443,7 +442,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxMove).await?;
|
||||
let create_mvbox = ctx.should_watch_mvbox().await?;
|
||||
|
||||
imap.configure_folders(ctx, create_mvbox).await?;
|
||||
|
||||
@@ -454,11 +453,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
drop(imap);
|
||||
|
||||
progress!(ctx, 910);
|
||||
// configuration success - write back the configured parameters with the
|
||||
// "configured_" prefix; also write the "configured"-flag */
|
||||
// the trailing underscore is correct
|
||||
param.save_to_database(ctx, "configured_").await?;
|
||||
ctx.sql.set_raw_config_bool("configured", true).await?;
|
||||
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
.await?;
|
||||
|
||||
@@ -476,6 +472,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
progress!(ctx, 940);
|
||||
update_device_chats_handle.await?;
|
||||
|
||||
ctx.sql.set_raw_config_bool("configured", true).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
109
src/constants.rs
109
src/constants.rs
@@ -4,6 +4,7 @@ use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::contact::ContactId;
|
||||
|
||||
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
|
||||
|
||||
@@ -179,11 +180,11 @@ pub const DC_ELLIPSIS: &str = "[...]";
|
||||
/// `char`s), not Unicode Grapheme Clusters.
|
||||
pub const DC_DESIRED_TEXT_LEN: usize = 5000;
|
||||
|
||||
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
|
||||
pub const DC_CONTACT_ID_SELF: u32 = 1;
|
||||
pub const DC_CONTACT_ID_INFO: u32 = 2;
|
||||
pub const DC_CONTACT_ID_DEVICE: u32 = 5;
|
||||
pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9;
|
||||
pub const DC_CONTACT_ID_UNDEFINED: ContactId = ContactId::new(0);
|
||||
pub const DC_CONTACT_ID_SELF: ContactId = ContactId::new(1);
|
||||
pub const DC_CONTACT_ID_INFO: ContactId = ContactId::new(2);
|
||||
pub const DC_CONTACT_ID_DEVICE: ContactId = ContactId::new(5);
|
||||
pub const DC_CONTACT_ID_LAST_SPECIAL: ContactId = ContactId::new(9);
|
||||
|
||||
// decorative address that is used for DC_CONTACT_ID_DEVICE
|
||||
// when an api that returns an email is called.
|
||||
@@ -230,79 +231,6 @@ pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
|
||||
// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db.
|
||||
pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Viewtype {
|
||||
Unknown = 0,
|
||||
|
||||
/// Text message.
|
||||
/// The text of the message is set using dc_msg_set_text()
|
||||
/// and retrieved with dc_msg_get_text().
|
||||
Text = 10,
|
||||
|
||||
/// Image message.
|
||||
/// If the image is an animated GIF, the type DC_MSG_GIF should be used.
|
||||
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
|
||||
/// and retrieved via dc_msg_set_file(), dc_msg_set_dimension().
|
||||
Image = 20,
|
||||
|
||||
/// Animated GIF message.
|
||||
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension()
|
||||
/// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height().
|
||||
Gif = 21,
|
||||
|
||||
/// Message containing a sticker, similar to image.
|
||||
/// If possible, the ui should display the image without borders in a transparent way.
|
||||
/// A click on a sticker will offer to install the sticker set in some future.
|
||||
Sticker = 23,
|
||||
|
||||
/// Message containing an Audio file.
|
||||
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
|
||||
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration().
|
||||
Audio = 40,
|
||||
|
||||
/// A voice message that was directly recorded by the user.
|
||||
/// For all other audio messages, the type #DC_MSG_AUDIO should be used.
|
||||
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
|
||||
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration()
|
||||
Voice = 41,
|
||||
|
||||
/// Video messages.
|
||||
/// File, width, height and durarion
|
||||
/// are set via dc_msg_set_file(), dc_msg_set_dimension(), dc_msg_set_duration()
|
||||
/// and retrieved via
|
||||
/// dc_msg_get_file(), dc_msg_get_width(),
|
||||
/// dc_msg_get_height(), dc_msg_get_duration().
|
||||
Video = 50,
|
||||
|
||||
/// Message containing any file, eg. a PDF.
|
||||
/// The file is set via dc_msg_set_file()
|
||||
/// and retrieved via dc_msg_get_file().
|
||||
File = 60,
|
||||
|
||||
/// Message is an invitation to a videochat.
|
||||
VideochatInvitation = 70,
|
||||
}
|
||||
|
||||
impl Default for Viewtype {
|
||||
fn default() -> Self {
|
||||
Viewtype::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
@@ -314,32 +242,9 @@ pub enum KeyType {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
#[test]
|
||||
fn test_derive_display_works_as_expected() {
|
||||
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_viewtype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::default());
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
|
||||
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
|
||||
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
|
||||
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
|
||||
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
|
||||
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
|
||||
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
|
||||
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
|
||||
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
|
||||
assert_eq!(
|
||||
Viewtype::VideochatInvitation,
|
||||
Viewtype::from_i32(70).unwrap()
|
||||
);
|
||||
}
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_chattype_values() {
|
||||
|
||||
159
src/contact.rs
159
src/contact.rs
@@ -1,12 +1,14 @@
|
||||
//! Contacts module
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::path::PathBuf;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::ChatId;
|
||||
@@ -27,6 +29,66 @@ use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::{chat, stock_str};
|
||||
|
||||
/// Contact ID, including reserved IDs.
|
||||
///
|
||||
/// Some contact IDs are reserved to identify special contacts. This
|
||||
/// type can represent both the special as well as normal contacts.
|
||||
#[derive(
|
||||
Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct ContactId(u32);
|
||||
|
||||
impl ContactId {
|
||||
/// Creates a new [`ContactId`].
|
||||
pub const fn new(id: u32) -> ContactId {
|
||||
ContactId(id)
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch, do not use.
|
||||
pub const fn to_u32(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
// if *self == DC_CONTACT_ID_UNDEFINED {
|
||||
// write!(f, "Contact#Undefined")
|
||||
// } else if *self == DC_CONTACT_ID_SELF {
|
||||
// write!(f, "Contact#Self")
|
||||
// } else if *self == DC_CONTACT_ID_INFO {
|
||||
// write!(f, "Contact#Info")
|
||||
// } else if *self == DC_CONTACT_ID_DEVICE {
|
||||
// write!(f, "Contact#Device")
|
||||
// } else if *self <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
// write!(f, "Contact#Special{}", self.0)
|
||||
// } else {
|
||||
// write!(f, "Contact#{}", self.0)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow converting [`ContactId`] to an SQLite type.
|
||||
impl rusqlite::types::ToSql for ContactId {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let val = rusqlite::types::Value::Integer(i64::from(self.0));
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow converting an SQLite integer directly into [`ContactId`].
|
||||
impl rusqlite::types::FromSql for ContactId {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).and_then(|val| {
|
||||
val.try_into()
|
||||
.map(ContactId::new)
|
||||
.map_err(|_| rusqlite::types::FromSqlError::OutOfRange(val))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An object representing a single contact in memory.
|
||||
///
|
||||
/// The contact object is not updated.
|
||||
@@ -48,7 +110,7 @@ pub struct Contact {
|
||||
/// `dc_set_config` using "addr".
|
||||
///
|
||||
/// Normal contact IDs are larger than these special ones (larger than DC_CONTACT_ID_LAST_SPECIAL).
|
||||
pub id: u32,
|
||||
pub id: ContactId,
|
||||
|
||||
/// Contact name. It is recommended to use `Contact::get_name`,
|
||||
/// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
|
||||
@@ -183,7 +245,7 @@ impl Default for VerifiedStatus {
|
||||
}
|
||||
|
||||
impl Contact {
|
||||
pub async fn load_from_db(context: &Context, contact_id: u32) -> Result<Self> {
|
||||
pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result<Self> {
|
||||
let mut contact = context
|
||||
.sql
|
||||
.query_row(
|
||||
@@ -191,7 +253,7 @@ impl Contact {
|
||||
c.authname, c.param, c.status
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
paramsv![contact_id],
|
||||
|row| {
|
||||
let name: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
@@ -245,18 +307,18 @@ impl Contact {
|
||||
}
|
||||
|
||||
/// Check if a contact is blocked.
|
||||
pub async fn is_blocked_load(context: &Context, id: u32) -> Result<bool> {
|
||||
pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result<bool> {
|
||||
let blocked = Self::load_from_db(context, id).await?.blocked;
|
||||
Ok(blocked)
|
||||
}
|
||||
|
||||
/// Block the given contact.
|
||||
pub async fn block(context: &Context, id: u32) -> Result<()> {
|
||||
pub async fn block(context: &Context, id: ContactId) -> Result<()> {
|
||||
set_block_contact(context, id, true).await
|
||||
}
|
||||
|
||||
/// Unblock the given contact.
|
||||
pub async fn unblock(context: &Context, id: u32) -> Result<()> {
|
||||
pub async fn unblock(context: &Context, id: ContactId) -> Result<()> {
|
||||
set_block_contact(context, id, false).await
|
||||
}
|
||||
|
||||
@@ -269,7 +331,7 @@ impl Contact {
|
||||
/// a bunch of addresses.
|
||||
///
|
||||
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
|
||||
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<u32> {
|
||||
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<ContactId> {
|
||||
let name = improve_single_line_input(name);
|
||||
ensure!(!addr.is_empty(), "Cannot create contact with empty address");
|
||||
|
||||
@@ -292,12 +354,12 @@ impl Contact {
|
||||
}
|
||||
|
||||
/// Mark messages from a contact as noticed.
|
||||
pub async fn mark_noticed(context: &Context, id: u32) -> Result<()> {
|
||||
pub async fn mark_noticed(context: &Context, id: ContactId) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
|
||||
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
|
||||
paramsv![MessageState::InNoticed, id, MessageState::InFresh],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -313,7 +375,7 @@ impl Contact {
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
min_origin: Origin,
|
||||
) -> Result<Option<u32>> {
|
||||
) -> Result<Option<ContactId>> {
|
||||
if addr.is_empty() {
|
||||
bail!("lookup_id_by_addr: empty address");
|
||||
}
|
||||
@@ -333,7 +395,7 @@ impl Contact {
|
||||
AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||
paramsv![
|
||||
addr_normalized,
|
||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||
DC_CONTACT_ID_LAST_SPECIAL,
|
||||
min_origin as u32,
|
||||
],
|
||||
)
|
||||
@@ -371,7 +433,7 @@ impl Contact {
|
||||
name: &str,
|
||||
addr: &str,
|
||||
mut origin: Origin,
|
||||
) -> Result<(u32, Modifier)> {
|
||||
) -> Result<(ContactId, Modifier)> {
|
||||
let mut sth_modified = Modifier::None;
|
||||
|
||||
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
|
||||
@@ -500,7 +562,7 @@ impl Contact {
|
||||
paramsv![Chattype::Single, isize::try_from(row_id)?]
|
||||
).await?;
|
||||
if let Some(chat_id) = chat_id {
|
||||
let contact = Contact::get_by_id(context, row_id as u32).await?;
|
||||
let contact = Contact::get_by_id(context, ContactId::new(row_id)).await?;
|
||||
let chat_name = contact.get_display_name();
|
||||
match context
|
||||
.sql
|
||||
@@ -557,7 +619,7 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
Ok((row_id, sth_modified))
|
||||
Ok((ContactId::new(row_id), sth_modified))
|
||||
}
|
||||
|
||||
/// Add a number of contacts.
|
||||
@@ -617,7 +679,7 @@ impl Contact {
|
||||
context: &Context,
|
||||
listflags: u32,
|
||||
query: Option<impl AsRef<str>>,
|
||||
) -> Result<Vec<u32>> {
|
||||
) -> Result<Vec<ContactId>> {
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
@@ -644,16 +706,16 @@ impl Contact {
|
||||
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
|
||||
paramsv![
|
||||
self_addr,
|
||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||
DC_CONTACT_ID_LAST_SPECIAL,
|
||||
Origin::IncomingReplyTo,
|
||||
s3str_like_cmd,
|
||||
s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 },
|
||||
],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
ret.push(id? as u32);
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
@@ -690,13 +752,13 @@ impl Contact {
|
||||
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
||||
paramsv![
|
||||
self_addr,
|
||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||
DC_CONTACT_ID_LAST_SPECIAL,
|
||||
Origin::IncomingReplyTo
|
||||
],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
ret.push(id? as u32);
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
@@ -767,7 +829,7 @@ impl Contact {
|
||||
}
|
||||
|
||||
/// Get blocked contacts.
|
||||
pub async fn get_all_blocked(context: &Context) -> Result<Vec<u32>> {
|
||||
pub async fn get_all_blocked(context: &Context) -> Result<Vec<ContactId>> {
|
||||
Contact::update_blocked_mailinglist_contacts(context)
|
||||
.await
|
||||
.context("cannot update blocked mailinglist contacts")?;
|
||||
@@ -776,8 +838,8 @@ impl Contact {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
|row| row.get::<_, u32>(0),
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL],
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
@@ -792,7 +854,7 @@ impl Contact {
|
||||
/// This function returns a string explaining the encryption state
|
||||
/// of the contact and if the connection is encrypted the
|
||||
/// fingerprints of the keys involved.
|
||||
pub async fn get_encrinfo(context: &Context, contact_id: u32) -> Result<String> {
|
||||
pub async fn get_encrinfo(context: &Context, contact_id: ContactId) -> Result<String> {
|
||||
ensure!(
|
||||
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
|
||||
"Can not provide encryption info for special contact"
|
||||
@@ -861,7 +923,7 @@ impl Contact {
|
||||
/// possible as the contact is in use. In this case, the contact can be blocked.
|
||||
///
|
||||
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
|
||||
pub async fn delete(context: &Context, contact_id: u32) -> Result<()> {
|
||||
pub async fn delete(context: &Context, contact_id: ContactId) -> Result<()> {
|
||||
ensure!(
|
||||
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
|
||||
"Can not delete special contact"
|
||||
@@ -871,17 +933,14 @@ impl Contact {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
paramsv![contact_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
if count_chats == 0 {
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM contacts WHERE id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
)
|
||||
.execute("DELETE FROM contacts WHERE id=?;", paramsv![contact_id])
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
@@ -907,7 +966,7 @@ impl Contact {
|
||||
/// For contact DC_CONTACT_ID_SELF (1), the function returns sth.
|
||||
/// like "Me" in the selected language and the email address
|
||||
/// defined by dc_set_config().
|
||||
pub async fn get_by_id(context: &Context, contact_id: u32) -> Result<Contact> {
|
||||
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Contact> {
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
|
||||
Ok(contact)
|
||||
@@ -919,7 +978,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET param=? WHERE id=?",
|
||||
paramsv![self.param.to_string(), self.id as i32],
|
||||
paramsv![self.param.to_string(), self.id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -931,14 +990,14 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET status=? WHERE id=?",
|
||||
paramsv![self.status, self.id as i32],
|
||||
paramsv![self.status, self.id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the ID of the contact.
|
||||
pub fn get_id(&self) -> u32 {
|
||||
pub fn get_id(&self) -> ContactId {
|
||||
self.id
|
||||
}
|
||||
|
||||
@@ -1065,7 +1124,7 @@ impl Contact {
|
||||
pub async fn addr_equals_contact(
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
) -> Result<bool> {
|
||||
if addr.is_empty() {
|
||||
return Ok(false);
|
||||
@@ -1091,13 +1150,13 @@ impl Contact {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id>?;",
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|
||||
paramsv![DC_CONTACT_ID_LAST_SPECIAL],
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> Result<bool> {
|
||||
pub async fn real_exists_by_id(context: &Context, contact_id: ContactId) -> Result<bool> {
|
||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -1106,7 +1165,7 @@ impl Contact {
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
paramsv![contact_id],
|
||||
)
|
||||
.await?;
|
||||
Ok(exists)
|
||||
@@ -1114,14 +1173,14 @@ impl Contact {
|
||||
|
||||
pub async fn scaleup_origin_by_id(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
origin: Origin,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
|
||||
paramsv![origin, contact_id as i32, origin],
|
||||
paramsv![origin, contact_id, origin],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1165,7 +1224,11 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) -> Result<()> {
|
||||
async fn set_block_contact(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
new_blocking: bool,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
|
||||
"Can't block special contact {}",
|
||||
@@ -1179,7 +1242,7 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||
paramsv![new_blocking as i32, contact_id as i32],
|
||||
paramsv![i32::from(new_blocking), contact_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1230,7 +1293,7 @@ WHERE type=? AND id IN (
|
||||
/// this typically happens if we see message with our own profile image, sent from another device.
|
||||
pub(crate) async fn set_profile_image(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
profile_image: &AvatarAction,
|
||||
was_encrypted: bool,
|
||||
) -> Result<()> {
|
||||
@@ -1277,7 +1340,7 @@ pub(crate) async fn set_profile_image(
|
||||
/// between Delta Chat devices.
|
||||
pub(crate) async fn set_status(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
status: String,
|
||||
encrypted: bool,
|
||||
has_chat_version: bool,
|
||||
@@ -1303,7 +1366,7 @@ pub(crate) async fn set_status(
|
||||
/// Updates last seen timestamp of the contact if it is earlier than the given `timestamp`.
|
||||
pub(crate) async fn update_last_seen(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
@@ -1474,7 +1537,7 @@ mod tests {
|
||||
Origin::IncomingReplyTo,
|
||||
)
|
||||
.await?;
|
||||
assert_ne!(id, 0);
|
||||
assert_ne!(id, ContactId::new(0));
|
||||
|
||||
let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "");
|
||||
@@ -1629,7 +1692,7 @@ mod tests {
|
||||
|
||||
// check SELF
|
||||
let contact = Contact::load_from_db(&t, DC_CONTACT_ID_SELF).await.unwrap();
|
||||
assert_eq!(DC_CONTACT_ID_SELF, 1);
|
||||
assert_eq!(DC_CONTACT_ID_SELF, ContactId::new(1));
|
||||
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
|
||||
assert_eq!(contact.get_addr(), ""); // we're not configured
|
||||
assert!(!contact.is_blocked());
|
||||
|
||||
123
src/context.rs
123
src/context.rs
@@ -24,7 +24,6 @@ use crate::login_param::LoginParam;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::quota::QuotaInfo;
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::securejoin::Bob;
|
||||
use crate::sql::Sql;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -42,12 +41,9 @@ impl Deref for Context {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InnerContext {
|
||||
/// Database file path
|
||||
pub(crate) dbfile: PathBuf,
|
||||
/// Blob directory path
|
||||
pub(crate) blobdir: PathBuf,
|
||||
pub(crate) sql: Sql,
|
||||
pub(crate) bob: Bob,
|
||||
pub(crate) last_smeared_timestamp: RwLock<i64>,
|
||||
pub(crate) running_state: RwLock<RunningState>,
|
||||
/// Mutex to avoid generating the key for the user more than once.
|
||||
@@ -84,7 +80,7 @@ pub struct InnerContext {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RunningState {
|
||||
pub ongoing_running: bool,
|
||||
ongoing_running: bool,
|
||||
shall_stop_ongoing: bool,
|
||||
cancel_sender: Option<Sender<()>>,
|
||||
}
|
||||
@@ -106,10 +102,19 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Creates new context.
|
||||
/// Creates new context and opens the database.
|
||||
pub async fn new(dbfile: PathBuf, id: u32) -> Result<Context> {
|
||||
// pretty_env_logger::try_init_timed().ok();
|
||||
let context = Self::new_closed(dbfile, id).await?;
|
||||
|
||||
// Open the database if is not encrypted.
|
||||
if context.check_passphrase("".to_string()).await? {
|
||||
context.sql.open(&context, "".to_string()).await?;
|
||||
}
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
/// Creates new context without opening the database.
|
||||
pub async fn new_closed(dbfile: PathBuf, id: u32) -> Result<Context> {
|
||||
let mut blob_fname = OsString::new();
|
||||
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
||||
blob_fname.push("-blobs");
|
||||
@@ -117,7 +122,35 @@ impl Context {
|
||||
if !blobdir.exists().await {
|
||||
async_std::fs::create_dir_all(&blobdir).await?;
|
||||
}
|
||||
Context::with_blobdir(dbfile, blobdir, id).await
|
||||
let context = Context::with_blobdir(dbfile, blobdir, id).await?;
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
/// Opens the database with the given passphrase.
|
||||
///
|
||||
/// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other
|
||||
/// errors.
|
||||
pub async fn open(&self, passphrase: String) -> Result<bool> {
|
||||
if self.sql.check_passphrase(passphrase.clone()).await? {
|
||||
self.sql.open(self, passphrase).await?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if database is open.
|
||||
pub async fn is_open(&self) -> bool {
|
||||
self.sql.is_open().await
|
||||
}
|
||||
|
||||
/// Tests the database passphrase.
|
||||
///
|
||||
/// Returns true if passphrase is correct.
|
||||
///
|
||||
/// Fails if database is already open.
|
||||
pub(crate) async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
|
||||
self.sql.check_passphrase(passphrase).await
|
||||
}
|
||||
|
||||
pub(crate) async fn with_blobdir(
|
||||
@@ -134,10 +167,8 @@ impl Context {
|
||||
let inner = InnerContext {
|
||||
id,
|
||||
blobdir,
|
||||
dbfile,
|
||||
running_state: RwLock::new(Default::default()),
|
||||
sql: Sql::new(),
|
||||
bob: Default::default(),
|
||||
sql: Sql::new(dbfile),
|
||||
last_smeared_timestamp: RwLock::new(0),
|
||||
generating_key_mutex: Mutex::new(()),
|
||||
oauth2_mutex: Mutex::new(()),
|
||||
@@ -155,7 +186,6 @@ impl Context {
|
||||
let ctx = Context {
|
||||
inner: Arc::new(inner),
|
||||
};
|
||||
ctx.sql.open(&ctx, &ctx.dbfile, false).await?;
|
||||
|
||||
Ok(ctx)
|
||||
}
|
||||
@@ -193,7 +223,7 @@ impl Context {
|
||||
|
||||
/// Returns database file path.
|
||||
pub fn get_dbfile(&self) -> &Path {
|
||||
self.dbfile.as_path()
|
||||
self.sql.dbfile.as_path()
|
||||
}
|
||||
|
||||
/// Returns blob directory path.
|
||||
@@ -224,7 +254,7 @@ impl Context {
|
||||
|
||||
// Ongoing process allocation/free/check
|
||||
|
||||
pub async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
|
||||
pub(crate) async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
|
||||
if self.has_ongoing().await {
|
||||
bail!("There is already another ongoing process running.");
|
||||
}
|
||||
@@ -240,7 +270,7 @@ impl Context {
|
||||
Ok(receiver)
|
||||
}
|
||||
|
||||
pub async fn free_ongoing(&self) {
|
||||
pub(crate) async fn free_ongoing(&self) {
|
||||
let s_a = &self.running_state;
|
||||
let mut s = s_a.write().await;
|
||||
|
||||
@@ -249,7 +279,7 @@ impl Context {
|
||||
s.cancel_sender.take();
|
||||
}
|
||||
|
||||
pub async fn has_ongoing(&self) -> bool {
|
||||
pub(crate) async fn has_ongoing(&self) -> bool {
|
||||
let s_a = &self.running_state;
|
||||
let s = s_a.read().await;
|
||||
|
||||
@@ -274,7 +304,7 @@ impl Context {
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn shall_stop_ongoing(&self) -> bool {
|
||||
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
|
||||
self.running_state.read().await.shall_stop_ongoing
|
||||
}
|
||||
|
||||
@@ -324,7 +354,7 @@ impl Context {
|
||||
|
||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
|
||||
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
|
||||
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
|
||||
let only_fetch_mvbox = self.get_config_int(Config::OnlyFetchMvbox).await?;
|
||||
let folders_configured = self
|
||||
.sql
|
||||
.get_raw_config_int("folders_configured")
|
||||
@@ -350,6 +380,13 @@ impl Context {
|
||||
res.insert("number_of_contacts", contacts.to_string());
|
||||
res.insert("database_dir", self.get_dbfile().display().to_string());
|
||||
res.insert("database_version", dbversion.to_string());
|
||||
res.insert(
|
||||
"database_encrypted",
|
||||
self.sql
|
||||
.is_encrypted()
|
||||
.await
|
||||
.map_or_else(|| "closed".to_string(), |b| b.to_string()),
|
||||
);
|
||||
res.insert("journal_mode", journal_mode);
|
||||
res.insert("blobdir", self.get_blobdir().display().to_string());
|
||||
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
|
||||
@@ -381,7 +418,7 @@ impl Context {
|
||||
);
|
||||
res.insert("sentbox_watch", sentbox_watch.to_string());
|
||||
res.insert("mvbox_move", mvbox_move.to_string());
|
||||
res.insert("sentbox_move", sentbox_move.to_string());
|
||||
res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string());
|
||||
res.insert("folders_configured", folders_configured.to_string());
|
||||
res.insert("configured_sentbox_folder", configured_sentbox_folder);
|
||||
res.insert("configured_mvbox_folder", configured_mvbox_folder);
|
||||
@@ -574,14 +611,14 @@ impl Context {
|
||||
Ok(spam.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
|
||||
pub(crate) fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
|
||||
let mut blob_fname = OsString::new();
|
||||
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
||||
blob_fname.push("-blobs");
|
||||
dbfile.with_file_name(blob_fname)
|
||||
}
|
||||
|
||||
pub fn derive_walfile(dbfile: &PathBuf) -> PathBuf {
|
||||
pub(crate) fn derive_walfile(dbfile: &PathBuf) -> PathBuf {
|
||||
let mut wal_fname = OsString::new();
|
||||
wal_fname.push(dbfile.file_name().unwrap_or_default());
|
||||
wal_fname.push("-wal");
|
||||
@@ -633,21 +670,26 @@ mod tests {
|
||||
use crate::chat::{
|
||||
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
|
||||
};
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::constants::DC_CONTACT_ID_SELF;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::test_utils::TestContext;
|
||||
use anyhow::Context as _;
|
||||
use std::time::Duration;
|
||||
use strum::IntoEnumIterator;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_wrong_db() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
async fn test_wrong_db() -> Result<()> {
|
||||
let tmp = tempfile::tempdir()?;
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
std::fs::write(&dbfile, b"123").unwrap();
|
||||
let res = Context::new(dbfile.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
std::fs::write(&dbfile, b"123")?;
|
||||
let res = Context::new(dbfile.into(), 1).await?;
|
||||
|
||||
// Broken database is indistinguishable from encrypted one.
|
||||
assert_eq!(res.is_open().await, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -999,4 +1041,29 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let id = 1;
|
||||
let context = Context::new_closed(dbfile.clone().into(), id)
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
drop(context);
|
||||
|
||||
let id = 2;
|
||||
let context = Context::new(dbfile.into(), id)
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.is_open().await, false);
|
||||
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
|
||||
assert_eq!(context.open("false".to_string()).await?, false);
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
189
src/dc_tools.rs
189
src/dc_tools.rs
@@ -13,7 +13,7 @@ use async_std::path::{Path, PathBuf};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{fs, io};
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use anyhow::Error;
|
||||
use chrono::{Local, TimeZone};
|
||||
use mailparse::dateparse;
|
||||
use mailparse::headers::Headers;
|
||||
@@ -21,10 +21,10 @@ use mailparse::MailHeaderMap;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::chat::{add_device_msg, add_device_msg_with_importance};
|
||||
use crate::constants::{Viewtype, DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
|
||||
use crate::constants::{DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::provider::get_provider_update_timestamp;
|
||||
use crate::stock_str;
|
||||
|
||||
@@ -71,7 +71,7 @@ pub(crate) fn dc_gm2local_offset() -> i64 {
|
||||
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
|
||||
the function may return negative values. */
|
||||
let lt = Local::now();
|
||||
lt.offset().local_minus_utc() as i64
|
||||
i64::from(lt.offset().local_minus_utc())
|
||||
}
|
||||
|
||||
// timesmearing
|
||||
@@ -195,44 +195,26 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time
|
||||
}
|
||||
|
||||
/* Message-ID tools */
|
||||
|
||||
/// Generate an ID. The generated ID should be as short and as unique as possible:
|
||||
/// - short, because it may also used as part of Message-ID headers or in QR codes
|
||||
/// - unique as two IDs generated on two devices should not be the same. However, collisions are not world-wide but only by the few contacts.
|
||||
/// IDs generated by this function are 66 bit wide and are returned as 11 base64 characters.
|
||||
///
|
||||
/// Additional information when used as a message-id or group-id:
|
||||
/// - for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as Gr.<grpid>.<random>@<random>
|
||||
/// - for INCOMING messages, the ID is taken from the Chat-Group-ID-header or from the Message-ID in the In-Reply-To: or References:-Header
|
||||
/// - the group-id should be a string with the characters [a-zA-Z0-9\-_]
|
||||
pub(crate) fn dc_create_id() -> String {
|
||||
/* generate an id. the generated ID should be as short and as unique as possible:
|
||||
- short, because it may also used as part of Message-ID headers or in QR codes
|
||||
- unique as two IDs generated on two devices should not be the same. However, collisions are not world-wide but only by the few contacts.
|
||||
IDs generated by this function are 66 bit wide and are returned as 11 base64 characters.
|
||||
If possible, RNG of OpenSSL is used.
|
||||
|
||||
Additional information when used as a message-id or group-id:
|
||||
- for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as Gr.<grpid>.<random>@<random>
|
||||
- for INCOMING messages, the ID is taken from the Chat-Group-ID-header or from the Message-ID in the In-Reply-To: or References:-Header
|
||||
- the group-id should be a string with the characters [a-zA-Z0-9\-_] */
|
||||
|
||||
// ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure.
|
||||
let mut rng = thread_rng();
|
||||
let buf: [u32; 3] = [rng.gen(), rng.gen(), rng.gen()];
|
||||
|
||||
encode_66bits_as_base64(buf[0usize], buf[1usize], buf[2usize])
|
||||
}
|
||||
// Generate 72 random bits.
|
||||
let mut arr = [0u8; 9];
|
||||
rng.fill(&mut arr[..]);
|
||||
|
||||
/// Encode 66 bits as a base64 string.
|
||||
/// This is useful for ID generating with short strings as we save 5 character
|
||||
/// in each id compared to 64 bit hex encoding. For a typical group ID, these
|
||||
/// are 10 characters (grpid+msgid):
|
||||
/// hex: 64 bit, 4 bits/character, length = 64/4 = 16 characters
|
||||
/// base64: 64 bit, 6 bits/character, length = 64/6 = 11 characters (plus 2 additional bits)
|
||||
/// Only the lower 2 bits of `fill` are used.
|
||||
fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
|
||||
let mut wrapped_writer = Vec::new();
|
||||
{
|
||||
let mut enc = base64::write::EncoderWriter::new(&mut wrapped_writer, base64::URL_SAFE);
|
||||
enc.write_u32::<BigEndian>(v1).unwrap();
|
||||
enc.write_u32::<BigEndian>(v2).unwrap();
|
||||
enc.write_u8(((fill & 0x3) as u8) << 6).unwrap();
|
||||
enc.finish().unwrap();
|
||||
}
|
||||
assert_eq!(wrapped_writer.pop(), Some(b'A')); // Remove last "A"
|
||||
String::from_utf8(wrapped_writer).unwrap()
|
||||
// Take 11 base64 characters containing 66 random bits.
|
||||
base64::encode(&arr).chars().take(11).collect()
|
||||
}
|
||||
|
||||
/// Function generates a Message-ID that can be used for a new outgoing message.
|
||||
@@ -354,63 +336,6 @@ pub async fn dc_delete_files_in_dir(context: &Context, path: impl AsRef<Path>) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn dc_copy_file(
|
||||
context: &Context,
|
||||
src_path: impl AsRef<Path>,
|
||||
dest_path: impl AsRef<Path>,
|
||||
) -> bool {
|
||||
let src_abs = dc_get_abs_path(context, &src_path);
|
||||
let mut src_file = match fs::File::open(&src_abs).await {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"failed to open for read '{}': {}",
|
||||
src_abs.display(),
|
||||
err
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let dest_abs = dc_get_abs_path(context, &dest_path);
|
||||
let mut dest_file = match fs::OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&dest_abs)
|
||||
.await
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"failed to open for write '{}': {}",
|
||||
dest_abs.display(),
|
||||
err
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match io::copy(&mut src_file, &mut dest_file).await {
|
||||
Ok(_) => true,
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"Cannot copy \"{}\" to \"{}\": {}",
|
||||
src_abs.display(),
|
||||
dest_abs.display(),
|
||||
err
|
||||
);
|
||||
{
|
||||
// Attempt to remove the failed file, swallow errors resulting from that.
|
||||
fs::remove_file(dest_abs).await.ok();
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn dc_create_folder(
|
||||
context: &Context,
|
||||
path: impl AsRef<Path>,
|
||||
@@ -508,33 +433,6 @@ pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Ok((temp_path, dest_path)) on success. The backup can then be written to temp_path. If the backup succeeded,
|
||||
/// it can be renamed to dest_path. This guarantees that the backup is complete.
|
||||
pub(crate) async fn get_next_backup_path(
|
||||
folder: impl AsRef<Path>,
|
||||
backup_time: i64,
|
||||
) -> Result<(PathBuf, PathBuf), Error> {
|
||||
let folder = PathBuf::from(folder.as_ref());
|
||||
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
|
||||
// Don't change this file name format, in has_backup() we use string comparison to determine which backup is newer:
|
||||
.format("delta-chat-backup-%Y-%m-%d")
|
||||
.to_string();
|
||||
|
||||
// 64 backup files per day should be enough for everyone
|
||||
for i in 0..64 {
|
||||
let mut tempfile = folder.clone();
|
||||
tempfile.push(format!("{}-{:02}.tar.part", stem, i));
|
||||
|
||||
let mut destfile = folder.clone();
|
||||
destfile.push(format!("{}-{:02}.tar", stem, i));
|
||||
|
||||
if !tempfile.exists().await && !destfile.exists().await {
|
||||
return Ok((tempfile, destfile));
|
||||
}
|
||||
}
|
||||
bail!("could not create backup file, disk full?");
|
||||
}
|
||||
|
||||
pub(crate) fn time() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
@@ -636,8 +534,8 @@ impl rusqlite::types::ToSql for EmailAddress {
|
||||
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
|
||||
pub(crate) fn improve_single_line_input(input: &str) -> String {
|
||||
input
|
||||
.replace("\n", " ")
|
||||
.replace("\r", " ")
|
||||
.replace('\n', " ")
|
||||
.replace('\r', " ")
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
@@ -864,26 +762,6 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
assert_eq!(buf.len(), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_66bits_as_base64() {
|
||||
assert_eq!(
|
||||
encode_66bits_as_base64(0x01234567, 0x89abcdef, 0),
|
||||
"ASNFZ4mrze8"
|
||||
);
|
||||
assert_eq!(
|
||||
encode_66bits_as_base64(0x01234567, 0x89abcdef, 1),
|
||||
"ASNFZ4mrze9"
|
||||
);
|
||||
assert_eq!(
|
||||
encode_66bits_as_base64(0x01234567, 0x89abcdef, 2),
|
||||
"ASNFZ4mrze-"
|
||||
);
|
||||
assert_eq!(
|
||||
encode_66bits_as_base64(0x01234567, 0x89abcdef, 3),
|
||||
"ASNFZ4mrze_"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_extract_grpid_from_rfc724_mid() {
|
||||
// Should return None if we pass invalid mid
|
||||
@@ -1025,20 +903,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
|
||||
assert!(dc_file_exist!(context, &abs_path).await);
|
||||
|
||||
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
|
||||
|
||||
// attempting to copy a second time should fail
|
||||
assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
|
||||
|
||||
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada").await, 7);
|
||||
|
||||
let buf = dc_read_file(context, "$BLOBDIR/dada").await.unwrap();
|
||||
|
||||
assert_eq!(buf.len(), 7);
|
||||
assert_eq!(&buf, b"content");
|
||||
|
||||
assert!(dc_delete_file(context, "$BLOBDIR/foobar").await);
|
||||
assert!(dc_delete_file(context, "$BLOBDIR/dada").await);
|
||||
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder")
|
||||
.await
|
||||
.is_ok());
|
||||
@@ -1164,7 +1029,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
maybe_warn_on_bad_time(&t, timestamp_past, get_provider_update_timestamp()).await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let device_chat_id = chats.get_chat_id(0).unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1202,7 +1067,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(device_chat_id, chats.get_chat_id(0));
|
||||
assert_eq!(device_chat_id, chats.get_chat_id(0).unwrap());
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1234,7 +1099,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let device_chat_id = chats.get_chat_id(0).unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1256,7 +1121,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let device_chat_id = chats.get_chat_id(0).unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1273,7 +1138,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0);
|
||||
let device_chat_id = chats.get_chat_id(0).unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::imap::{Imap, ImapActionResult};
|
||||
use crate::job::{self, Action, Job, Status};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::param::Params;
|
||||
use crate::{job_try, stock_str, EventType};
|
||||
@@ -146,7 +146,7 @@ impl Job {
|
||||
|
||||
if let Some((server_uid, server_folder)) = row {
|
||||
match imap
|
||||
.fetch_single_msg(context, &server_folder, server_uid)
|
||||
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
@@ -185,6 +185,7 @@ impl Imap {
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
uid: u32,
|
||||
rfc724_mid: String,
|
||||
) -> ImapActionResult {
|
||||
if let Some(imapresult) = self
|
||||
.prepare_imap_operation_on_msg(context, folder, uid)
|
||||
@@ -196,9 +197,15 @@ impl Imap {
|
||||
// we are connected, and the folder is selected
|
||||
info!(context, "Downloading message {}/{} fully...", folder, uid);
|
||||
|
||||
let (last_uid, _received) = self
|
||||
.fetch_many_msgs(context, folder, vec![uid], false, false)
|
||||
.await;
|
||||
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
|
||||
uid_message_ids.insert(uid, rfc724_mid);
|
||||
let (last_uid, _received) = match self
|
||||
.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, false, false)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(_) => return ImapActionResult::Failed,
|
||||
};
|
||||
if last_uid.is_none() {
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
@@ -247,13 +254,15 @@ impl MimeMessage {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::chat::send_msg;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::dc_receive_imf::dc_receive_imf_inner;
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::message::Viewtype;
|
||||
use crate::test_utils::TestContext;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
@@ -333,7 +342,16 @@ mod tests {
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\
|
||||
Content-Type: text/plain";
|
||||
|
||||
dc_receive_imf_inner(&t, header.as_bytes(), "INBOX", false, Some(100000), false).await?;
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
"Mr.12345678901@example.com",
|
||||
header.as_bytes(),
|
||||
"INBOX",
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
@@ -344,6 +362,7 @@ mod tests {
|
||||
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
"Mr.12345678901@example.com",
|
||||
format!("{}\n\n100k text...", header).as_bytes(),
|
||||
"INBOX",
|
||||
false,
|
||||
@@ -373,6 +392,7 @@ mod tests {
|
||||
// download message from bob partially, this must not change the ephemeral timer
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
"first@example.org",
|
||||
b"From: Bob <bob@example.org>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
|
||||
25
src/e2ee.rs
25
src/e2ee.rs
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{bail, format_err, Result};
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use mailparse::ParsedMail;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
@@ -121,9 +121,9 @@ impl EncryptHelper {
|
||||
.into_iter()
|
||||
.filter_map(|(state, addr)| state.map(|s| (s, addr)))
|
||||
{
|
||||
let key = peerstate.take_key(min_verified).ok_or_else(|| {
|
||||
format_err!("proper enc-key for {} missing, cannot encrypt", addr)
|
||||
})?;
|
||||
let key = peerstate
|
||||
.take_key(min_verified)
|
||||
.with_context(|| format!("proper enc-key for {} missing, cannot encrypt", addr))?;
|
||||
keyring.add(key);
|
||||
}
|
||||
keyring.add(self.public_key.clone());
|
||||
@@ -390,27 +390,24 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
format_err!(concat!(
|
||||
"Failed to get self address, ",
|
||||
"cannot ensure secret key if not configured."
|
||||
))
|
||||
})?;
|
||||
.context(concat!(
|
||||
"Failed to get self address, ",
|
||||
"cannot ensure secret key if not configured."
|
||||
))?;
|
||||
SignedPublicKey::load_self(context).await?;
|
||||
Ok(self_addr)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::ToSave;
|
||||
use crate::test_utils::{bob_keypair, TestContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
mod ensure_secret_key_exists {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -67,14 +67,16 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{send_msg, ChatId};
|
||||
use crate::constants::{
|
||||
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
|
||||
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
|
||||
};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
||||
use crate::events::EventType;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use std::cmp::max;
|
||||
|
||||
@@ -212,7 +214,7 @@ impl ChatId {
|
||||
pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
context: &Context,
|
||||
timer: Timer,
|
||||
from_id: u32,
|
||||
from_id: ContactId,
|
||||
) -> String {
|
||||
match timer {
|
||||
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
|
||||
@@ -297,6 +299,37 @@ impl MsgId {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
context: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
) -> Result<()> {
|
||||
let msg_ids: Vec<&dyn crate::ToSql> = msg_ids
|
||||
.iter()
|
||||
.map(|msg_id| msg_id as &dyn crate::ToSql)
|
||||
.collect();
|
||||
let now = time();
|
||||
let count = context
|
||||
.sql
|
||||
.execute(
|
||||
format!(
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? + ephemeral_timer
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ? + ephemeral_timer) AND ephemeral_timer > 0
|
||||
AND id IN ({})",
|
||||
sql::repeat_vars(msg_ids.len())?
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
std::iter::once(&now as &dyn crate::ToSql)
|
||||
.chain(std::iter::once(&now as &dyn crate::ToSql))
|
||||
.chain(msg_ids),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
if count > 0 {
|
||||
schedule_ephemeral_task(context).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes messages which are expired according to
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
@@ -451,12 +484,11 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
|
||||
.execute(
|
||||
"UPDATE imap
|
||||
SET target=''
|
||||
WHERE EXISTS (
|
||||
SELECT * FROM msgs
|
||||
WHERE rfc724_mid=imap.rfc724_mid
|
||||
AND ((download_state = 0 AND timestamp < ?) OR
|
||||
(download_state != 0 AND timestamp < ?) OR
|
||||
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
|
||||
WHERE rfc724_mid IN (
|
||||
SELECT rfc724_mid FROM msgs
|
||||
WHERE ((download_state = 0 AND timestamp < ?) OR
|
||||
(download_state != 0 AND timestamp < ?) OR
|
||||
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
|
||||
)",
|
||||
paramsv![threshold_timestamp, threshold_timestamp_extended, now],
|
||||
)
|
||||
@@ -824,8 +856,8 @@ mod tests {
|
||||
|
||||
// Check that if there is a message left, the text and metadata are gone
|
||||
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
|
||||
assert_eq!(msg.from_id, 0);
|
||||
assert_eq!(msg.to_id, 0);
|
||||
assert_eq!(msg.from_id, ContactId::new(0));
|
||||
assert_eq!(msg.to_id, ContactId::new(0));
|
||||
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
|
||||
let rawtxt: Option<String> = t
|
||||
.sql
|
||||
|
||||
@@ -7,6 +7,7 @@ use async_std::path::PathBuf;
|
||||
use strum::EnumProperty;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::contact::ContactId;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::message::MsgId;
|
||||
|
||||
@@ -252,7 +253,7 @@ pub enum EventType {
|
||||
///
|
||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
#[strum(props(id = "2030"))]
|
||||
ContactsChanged(Option<u32>),
|
||||
ContactsChanged(Option<ContactId>),
|
||||
|
||||
/// Location of one or more contact has changed.
|
||||
///
|
||||
@@ -260,7 +261,7 @@ pub enum EventType {
|
||||
/// If the locations of several contacts have been changed,
|
||||
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
|
||||
#[strum(props(id = "2035"))]
|
||||
LocationChanged(Option<u32>),
|
||||
LocationChanged(Option<ContactId>),
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
#[strum(props(id = "2041"))]
|
||||
@@ -304,7 +305,10 @@ pub enum EventType {
|
||||
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||
/// 1000=Protocol finished for this contact.
|
||||
#[strum(props(id = "2060"))]
|
||||
SecurejoinInviterProgress { contact_id: u32, progress: usize },
|
||||
SecurejoinInviterProgress {
|
||||
contact_id: ContactId,
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the joiner
|
||||
/// (Bob, the person who scans the QR code).
|
||||
@@ -315,7 +319,10 @@ pub enum EventType {
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
#[strum(props(id = "2061"))]
|
||||
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||
SecurejoinJoinerProgress {
|
||||
contact_id: ContactId,
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// The connectivity to the server changed.
|
||||
/// This means that you should refresh the connectivity view
|
||||
@@ -326,4 +333,7 @@ pub enum EventType {
|
||||
|
||||
#[strum(props(id = "2110"))]
|
||||
SelfavatarChanged,
|
||||
|
||||
#[strum(props(id = "2120"))]
|
||||
WebxdcStatusUpdate(MsgId),
|
||||
}
|
||||
|
||||
10
src/html.rs
10
src/html.rs
@@ -279,9 +279,9 @@ mod tests {
|
||||
use crate::chat;
|
||||
use crate::chat::forward_msgs;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::constants::DC_CONTACT_ID_SELF;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::MessengerMessage;
|
||||
use crate::message::{MessengerMessage, Viewtype};
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[async_std::test]
|
||||
@@ -365,7 +365,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
// however, rust multiline-strings use just `\n`;
|
||||
// therefore, we just remove `\r` before comparison.
|
||||
assert_eq!(
|
||||
parser.html.replace("\r", ""),
|
||||
parser.html.replace('\r', ""),
|
||||
r##"
|
||||
<html>
|
||||
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
|
||||
@@ -379,7 +379,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html.replace("\r", ""), // see comment in test_htmlparse_html()
|
||||
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
|
||||
r##"<html>
|
||||
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
|
||||
</html>
|
||||
@@ -394,7 +394,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html.replace("\r", ""), // see comment in test_htmlparse_html()
|
||||
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
|
||||
r##"<html>
|
||||
<p>
|
||||
this is <b>html</b>
|
||||
|
||||
1065
src/imap.rs
1065
src/imap.rs
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
use super::Imap;
|
||||
|
||||
use anyhow::{bail, format_err, Result};
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use async_std::prelude::*;
|
||||
use std::time::{Duration, SystemTime};
|
||||
@@ -31,7 +31,7 @@ impl Imap {
|
||||
let timeout = Duration::from_secs(23 * 60);
|
||||
let mut info = Default::default();
|
||||
|
||||
if self.server_sent_unsolicited_exists(context) {
|
||||
if self.server_sent_unsolicited_exists(context)? {
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
@@ -90,8 +90,8 @@ impl Imap {
|
||||
let session = handle
|
||||
.done()
|
||||
.timeout(Duration::from_secs(15))
|
||||
.await
|
||||
.map_err(|err| format_err!("IMAP IDLE protocol timed out: {}", err))??;
|
||||
.await?
|
||||
.context("IMAP IDLE protocol timed out")?;
|
||||
self.session = Some(Session { inner: session });
|
||||
} else {
|
||||
warn!(context, "Attempted to idle without a session");
|
||||
@@ -157,7 +157,6 @@ impl Imap {
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
|
||||
match self.fetch_new_messages(context, &watch_folder, false).await {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
|
||||
@@ -2,15 +2,18 @@ use std::{collections::BTreeMap, time::Instant};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::imap::Imap;
|
||||
use crate::{config::Config, log::LogExt};
|
||||
use crate::log::LogExt;
|
||||
use crate::{context::Context, imap::FolderMeaning};
|
||||
use async_std::prelude::*;
|
||||
|
||||
use async_std::stream::StreamExt;
|
||||
|
||||
use super::{get_folder_meaning, get_folder_meaning_by_name};
|
||||
|
||||
impl Imap {
|
||||
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<()> {
|
||||
/// Returns true if folders were scanned, false if scanning was postponed.
|
||||
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<bool> {
|
||||
// First of all, debounce to once per minute:
|
||||
let mut last_scan = context.last_full_folder_scan.lock().await;
|
||||
if let Some(last_scan) = *last_scan {
|
||||
@@ -20,28 +23,18 @@ impl Imap {
|
||||
.await?;
|
||||
|
||||
if elapsed_secs < debounce_secs {
|
||||
return Ok(());
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
info!(context, "Starting full folder scan");
|
||||
|
||||
self.prepare(context).await?;
|
||||
let session = self.session.as_mut();
|
||||
let session = session.context("scan_folders(): IMAP No Connection established")?;
|
||||
let folders: Vec<_> = session.list(Some(""), Some("*")).await?.collect().await;
|
||||
let folders = self.list_folders(context).await?;
|
||||
let watched_folders = get_watched_folders(context).await?;
|
||||
|
||||
let mut folder_configs = BTreeMap::new();
|
||||
|
||||
for folder in folders {
|
||||
let folder = match folder {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
warn!(context, "Can't get folder: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Gmail labels are not folders and should be skipped. For example,
|
||||
// emails appear in the inbox and under "All Mail" as soon as it is
|
||||
// received. The code used to wrongly conclude that the email had
|
||||
@@ -71,7 +64,7 @@ impl Imap {
|
||||
// Don't scan folders that are watched anyway
|
||||
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
|
||||
// Drain leftover unsolicited EXISTS messages
|
||||
self.server_sent_unsolicited_exists(context);
|
||||
self.server_sent_unsolicited_exists(context)?;
|
||||
|
||||
loop {
|
||||
self.fetch_move_delete(context, folder.name())
|
||||
@@ -79,7 +72,7 @@ impl Imap {
|
||||
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
|
||||
|
||||
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
|
||||
if !self.server_sent_unsolicited_exists(context) {
|
||||
if !self.server_sent_unsolicited_exists(context)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -98,24 +91,40 @@ impl Imap {
|
||||
}
|
||||
|
||||
last_scan.replace(Instant::now());
|
||||
Ok(())
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Returns the names of all folders on the IMAP server.
|
||||
pub async fn list_folders(
|
||||
self: &mut Imap,
|
||||
context: &Context,
|
||||
) -> Result<Vec<async_imap::types::Name>> {
|
||||
let session = self.session.as_mut();
|
||||
let session = session.context("No IMAP connection")?;
|
||||
let list = session
|
||||
.list(Some(""), Some("*"))
|
||||
.await?
|
||||
.filter_map(|f| f.ok_or_log_msg(context, "list_folders() can't get folder"));
|
||||
Ok(list.collect().await)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
|
||||
let mut res = vec![Config::ConfiguredInboxFolder];
|
||||
if context.get_config_bool(Config::SentboxWatch).await? {
|
||||
res.push(Config::ConfiguredSentboxFolder);
|
||||
}
|
||||
if context.should_watch_mvbox().await? {
|
||||
res.push(Config::ConfiguredMvboxFolder);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
|
||||
let mut res = Vec::new();
|
||||
if let Some(inbox_folder) = context.get_config(Config::ConfiguredInboxFolder).await? {
|
||||
res.push(inbox_folder);
|
||||
}
|
||||
let folder_watched_configured = &[
|
||||
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),
|
||||
(Config::MvboxMove, Config::ConfiguredMvboxFolder),
|
||||
];
|
||||
for (watched, configured) in folder_watched_configured {
|
||||
if context.get_config_bool(*watched).await? {
|
||||
if let Some(folder) = context.get_config(*configured).await? {
|
||||
res.push(folder);
|
||||
}
|
||||
for folder_config in get_watched_folder_configs(context).await? {
|
||||
if let Some(folder) = context.get_config(folder_config).await? {
|
||||
res.push(folder);
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
|
||||
@@ -93,7 +93,11 @@ impl Imap {
|
||||
// select new folder
|
||||
if let Some(folder) = folder {
|
||||
if let Some(ref mut session) = &mut self.session {
|
||||
let res = session.select(folder).await;
|
||||
let res = if self.config.can_condstore {
|
||||
session.select_condstore(folder).await
|
||||
} else {
|
||||
session.select(folder).await
|
||||
};
|
||||
|
||||
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
|
||||
// says that if the server reports select failure we are in
|
||||
|
||||
402
src/imex.rs
402
src/imex.rs
@@ -16,21 +16,21 @@ use rand::{thread_rng, Rng};
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::constants::DC_CONTACT_ID_SELF;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{
|
||||
dc_copy_file, dc_create_folder, dc_delete_file, dc_delete_files_in_dir, dc_get_filesuffix_lc,
|
||||
dc_open_file_std, dc_read_file, dc_write_file, get_next_backup_path, time, EmailAddress,
|
||||
dc_create_folder, dc_delete_file, dc_delete_files_in_dir, dc_get_filesuffix_lc,
|
||||
dc_open_file_std, dc_read_file, dc_write_file, time, EmailAddress,
|
||||
};
|
||||
use crate::e2ee;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::pgp;
|
||||
use crate::sql::{self, Sql};
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
|
||||
// Name of the database file in the backup.
|
||||
@@ -41,24 +41,24 @@ const BLOBS_BACKUP_NAME: &str = "blobs_backup";
|
||||
#[repr(u32)]
|
||||
pub enum ImexMode {
|
||||
/// Export all private keys and all public keys of the user to the
|
||||
/// directory given as `param1`. The default key is written to the files `public-key-default.asc`
|
||||
/// directory given as `path`. The default key is written to the files `public-key-default.asc`
|
||||
/// and `private-key-default.asc`, if there are more keys, they are written to files as
|
||||
/// `public-key-<id>.asc` and `private-key-<id>.asc`
|
||||
ExportSelfKeys = 1,
|
||||
|
||||
/// Import private keys found in the directory given as `param1`.
|
||||
/// Import private keys found in the directory given as `path`.
|
||||
/// The last imported key is made the default keys unless its name contains the string `legacy`.
|
||||
/// Public keys are not imported.
|
||||
ImportSelfKeys = 2,
|
||||
|
||||
/// Export a backup to the directory given as `param1`.
|
||||
/// Export a backup to the directory given as `path` with the given `passphrase`.
|
||||
/// The backup contains all contacts, chats, images and other data and device independent settings.
|
||||
/// The backup does not contain device dependent settings as ringtones or LED notification settings.
|
||||
/// The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
|
||||
/// the format is `delta-chat-<day>-<number>.tar`
|
||||
ExportBackup = 11,
|
||||
|
||||
/// `param1` is the file (not: directory) to import. The file is normally
|
||||
/// `path` is the file (not: directory) to import. The file is normally
|
||||
/// created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
|
||||
/// is only possible as long as the context is not configured or used in another way.
|
||||
ImportBackup = 12,
|
||||
@@ -78,11 +78,16 @@ pub enum ImexMode {
|
||||
///
|
||||
/// Only one import-/export-progress can run at the same time.
|
||||
/// To cancel an import-/export-progress, drop the future returned by this function.
|
||||
pub async fn imex(context: &Context, what: ImexMode, param1: &Path) -> Result<()> {
|
||||
pub async fn imex(
|
||||
context: &Context,
|
||||
what: ImexMode,
|
||||
path: &Path,
|
||||
passphrase: Option<String>,
|
||||
) -> Result<()> {
|
||||
let cancel = context.alloc_ongoing().await?;
|
||||
|
||||
let res = async {
|
||||
let success = imex_inner(context, what, param1).await;
|
||||
let success = imex_inner(context, what, path, passphrase).await;
|
||||
match success {
|
||||
Ok(()) => {
|
||||
info!(context, "IMEX successfully completed");
|
||||
@@ -115,15 +120,10 @@ async fn cleanup_aborted_imex(context: &Context, what: ImexMode) {
|
||||
dc_delete_file(context, context.get_dbfile()).await;
|
||||
dc_delete_files_in_dir(context, context.get_blobdir()).await;
|
||||
}
|
||||
if what == ImexMode::ExportBackup || what == ImexMode::ImportBackup {
|
||||
if let Err(e) = context.sql.open(context, context.get_dbfile(), false).await {
|
||||
warn!(context, "Re-opening db after imex failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the filename of the backup found (otherwise an error)
|
||||
pub async fn has_backup(context: &Context, dir_name: &Path) -> Result<String> {
|
||||
pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
|
||||
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
|
||||
let mut newest_backup_name = "".to_string();
|
||||
let mut newest_backup_path: Option<PathBuf> = None;
|
||||
@@ -145,59 +145,6 @@ pub async fn has_backup(context: &Context, dir_name: &Path) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
match newest_backup_path {
|
||||
Some(path) => Ok(path.to_string_lossy().into_owned()),
|
||||
None => has_backup_old(context, dir_name).await,
|
||||
// When we decide to remove support for .bak backups, we can replace this with `None => bail!("no backup found in {}", dir_name.display()),`.
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the filename of the backup found (otherwise an error)
|
||||
pub async fn has_backup_old(context: &Context, dir_name: &Path) -> Result<String> {
|
||||
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
|
||||
let mut newest_backup_time = 0;
|
||||
let mut newest_backup_name = "".to_string();
|
||||
let mut newest_backup_path: Option<PathBuf> = None;
|
||||
while let Some(dirent) = dir_iter.next().await {
|
||||
if let Ok(dirent) = dirent {
|
||||
let path = dirent.path();
|
||||
let name = dirent.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if name.starts_with("delta-chat") && name.ends_with(".bak") {
|
||||
let sql = Sql::new();
|
||||
match sql.open(context, &path, true).await {
|
||||
Ok(_) => {
|
||||
let curr_backup_time = sql
|
||||
.get_raw_config_int("backup_time")
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
if curr_backup_time > newest_backup_time {
|
||||
newest_backup_path = Some(path);
|
||||
newest_backup_time = curr_backup_time;
|
||||
}
|
||||
info!(context, "backup_time of {} is {}", name, curr_backup_time);
|
||||
sql.close().await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
context,
|
||||
"Found backup file {} which could not be opened: {}", name, e
|
||||
);
|
||||
// On some Android devices we can't open sql files that are not in our private directory
|
||||
// (see <https://github.com/deltachat/deltachat-android/issues/1768>). So, compare names
|
||||
// to still find the newest backup.
|
||||
let name: String = name.into();
|
||||
if newest_backup_time == 0
|
||||
&& (newest_backup_name.is_empty() || name > newest_backup_name)
|
||||
{
|
||||
newest_backup_path = Some(path);
|
||||
newest_backup_name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match newest_backup_path {
|
||||
Some(path) => Ok(path.to_string_lossy().into_owned()),
|
||||
None => bail!("no backup found in {}", dir_name.display()),
|
||||
@@ -218,7 +165,6 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
}
|
||||
|
||||
async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
let mut msg: Message;
|
||||
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?;
|
||||
@@ -231,8 +177,10 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
.await?;
|
||||
|
||||
let chat_id = ChatId::create_for_contact(context, DC_CONTACT_ID_SELF).await?;
|
||||
msg = Message::default();
|
||||
msg.viewtype = Viewtype::File;
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::File,
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set(Param::File, setup_file_blob.as_name());
|
||||
msg.subject = stock_str::ac_setup_msg_subject(context).await;
|
||||
msg.param
|
||||
@@ -289,7 +237,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
|
||||
|
||||
let msg_subj = stock_str::ac_setup_msg_subject(context).await;
|
||||
let msg_body = stock_str::ac_setup_msg_body(context).await;
|
||||
let msg_body_html = msg_body.replace("\r", "").replace("\n", "<br>");
|
||||
let msg_body_html = msg_body.replace('\r', "").replace('\n', "<br>");
|
||||
Ok(format!(
|
||||
concat!(
|
||||
"<!DOCTYPE html>\r\n",
|
||||
@@ -449,7 +397,12 @@ fn normalize_setup_code(s: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
async fn imex_inner(context: &Context, what: ImexMode, path: &Path) -> Result<()> {
|
||||
async fn imex_inner(
|
||||
context: &Context,
|
||||
what: ImexMode,
|
||||
path: &Path,
|
||||
passphrase: Option<String>,
|
||||
) -> Result<()> {
|
||||
info!(context, "Import/export dir: {}", path.display());
|
||||
ensure!(context.sql.is_open().await, "Database not opened.");
|
||||
context.emit_event(EventType::ImexProgress(10));
|
||||
@@ -467,26 +420,27 @@ async fn imex_inner(context: &Context, what: ImexMode, path: &Path) -> Result<()
|
||||
ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
|
||||
ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
|
||||
|
||||
ImexMode::ExportBackup => export_backup(context, path).await,
|
||||
// import_backup() will call import_backup_old() if this is an old backup.
|
||||
ImexMode::ImportBackup => import_backup(context, path).await,
|
||||
ImexMode::ExportBackup => {
|
||||
export_backup(context, path, passphrase.unwrap_or_default()).await
|
||||
}
|
||||
ImexMode::ImportBackup => {
|
||||
import_backup(context, path, passphrase.unwrap_or_default()).await?;
|
||||
context.sql.run_migrations(context).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Import Backup
|
||||
async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()> {
|
||||
if backup_to_import.to_string_lossy().ends_with(".bak") {
|
||||
// Backwards compability
|
||||
return import_backup_old(context, backup_to_import).await;
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Import \"{}\" to \"{}\".",
|
||||
backup_to_import.display(),
|
||||
context.get_dbfile().display()
|
||||
);
|
||||
|
||||
/// Imports backup into the currently open database.
|
||||
///
|
||||
/// The contents of the currently open database will be lost.
|
||||
///
|
||||
/// `passphrase` is the passphrase used to open backup database. If backup is unencrypted, pass
|
||||
/// empty string here.
|
||||
async fn import_backup(
|
||||
context: &Context,
|
||||
backup_to_import: &Path,
|
||||
passphrase: String,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!context.is_configured().await?,
|
||||
"Cannot import backups to accounts in use."
|
||||
@@ -495,15 +449,19 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()>
|
||||
!context.scheduler.read().await.is_running(),
|
||||
"cannot import backup, IO already running"
|
||||
);
|
||||
context.sql.close().await;
|
||||
dc_delete_file(context, context.get_dbfile()).await;
|
||||
ensure!(
|
||||
!context.get_dbfile().exists().await,
|
||||
"Cannot delete old database."
|
||||
);
|
||||
|
||||
let backup_file = File::open(backup_to_import).await?;
|
||||
let file_size = backup_file.metadata().await?.len();
|
||||
info!(
|
||||
context,
|
||||
"Import \"{}\" ({} bytes) to \"{}\".",
|
||||
backup_to_import.display(),
|
||||
file_size,
|
||||
context.get_dbfile().display()
|
||||
);
|
||||
|
||||
context.sql.config_cache.write().await.clear();
|
||||
|
||||
let archive = Archive::new(backup_file);
|
||||
|
||||
let mut entries = archive.entries()?;
|
||||
@@ -522,11 +480,15 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()>
|
||||
if f.path()?.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
|
||||
// async_tar can't unpack to a specified file name, so we just unpack to the blobdir and then move the unpacked file.
|
||||
f.unpack_in(context.get_blobdir()).await?;
|
||||
fs::rename(
|
||||
context.get_blobdir().join(DBFILE_BACKUP_NAME),
|
||||
context.get_dbfile(),
|
||||
)
|
||||
.await?;
|
||||
let unpacked_database = context.get_blobdir().join(DBFILE_BACKUP_NAME);
|
||||
context
|
||||
.sql
|
||||
.import(&unpacked_database, passphrase.clone())
|
||||
.await
|
||||
.context("cannot import unpacked database")?;
|
||||
fs::remove_file(unpacked_database)
|
||||
.await
|
||||
.context("cannot remove unpacked database")?;
|
||||
} else {
|
||||
// async_tar will unpack to blobdir/BLOBS_BACKUP_NAME, so we move the file afterwards.
|
||||
f.unpack_in(context.get_blobdir()).await?;
|
||||
@@ -541,136 +503,52 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()>
|
||||
}
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
.open(context, context.get_dbfile(), false)
|
||||
.await
|
||||
.context("Could not re-open db")?;
|
||||
|
||||
delete_and_reset_all_device_msgs(context).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn import_backup_old(context: &Context, backup_to_import: &Path) -> Result<()> {
|
||||
info!(
|
||||
context,
|
||||
"Import \"{}\" to \"{}\".",
|
||||
backup_to_import.display(),
|
||||
context.get_dbfile().display()
|
||||
);
|
||||
|
||||
ensure!(
|
||||
!context.is_configured().await?,
|
||||
"Cannot import backups to accounts in use."
|
||||
);
|
||||
ensure!(
|
||||
!context.scheduler.read().await.is_running(),
|
||||
"cannot import backup, IO already running"
|
||||
);
|
||||
context.sql.close().await;
|
||||
dc_delete_file(context, context.get_dbfile()).await;
|
||||
ensure!(
|
||||
!context.get_dbfile().exists().await,
|
||||
"Cannot delete old database."
|
||||
);
|
||||
|
||||
ensure!(
|
||||
dc_copy_file(context, backup_to_import, context.get_dbfile()).await,
|
||||
"could not copy file"
|
||||
);
|
||||
/* error already logged */
|
||||
/* re-open copied database file */
|
||||
context
|
||||
.sql
|
||||
.open(context, context.get_dbfile(), false)
|
||||
.await
|
||||
.context("Could not re-open db")?;
|
||||
|
||||
delete_and_reset_all_device_msgs(context).await?;
|
||||
|
||||
let total_files_cnt = context
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM backup_blobs;", paramsv![])
|
||||
.await?;
|
||||
info!(
|
||||
context,
|
||||
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
|
||||
);
|
||||
|
||||
// Load IDs only for now, without the file contents, to avoid
|
||||
// consuming too much memory.
|
||||
let file_ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM backup_blobs ORDER BY id",
|
||||
paramsv![],
|
||||
|row| row.get(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<i64>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut all_files_extracted = true;
|
||||
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
|
||||
// Load a single blob into memory
|
||||
let (file_name, file_blob) = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
|
||||
paramsv![file_id],
|
||||
|row| {
|
||||
let file_name: String = row.get(0)?;
|
||||
let file_blob: Vec<u8> = row.get(1)?;
|
||||
Ok((file_name, file_blob))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if context.shall_stop_ongoing().await {
|
||||
all_files_extracted = false;
|
||||
break;
|
||||
}
|
||||
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
|
||||
if permille < 10 {
|
||||
permille = 10
|
||||
}
|
||||
if permille > 990 {
|
||||
permille = 990
|
||||
}
|
||||
context.emit_event(EventType::ImexProgress(permille));
|
||||
if file_blob.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path_filename = context.get_blobdir().join(file_name);
|
||||
dc_write_file(context, &path_filename, &file_blob).await?;
|
||||
}
|
||||
|
||||
if all_files_extracted {
|
||||
// only delete backup_blobs if all files were successfully extracted
|
||||
context
|
||||
.sql
|
||||
.execute("DROP TABLE backup_blobs;", paramsv![])
|
||||
.await?;
|
||||
context.sql.execute("VACUUM;", paramsv![]).await.ok();
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("received stop signal");
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* Export backup
|
||||
******************************************************************************/
|
||||
#[allow(unused)]
|
||||
async fn export_backup(context: &Context, dir: &Path) -> Result<()> {
|
||||
|
||||
/// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be
|
||||
/// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded,
|
||||
/// it can be renamed to dest_path. This guarantees that the backup is complete.
|
||||
async fn get_next_backup_path(
|
||||
folder: &Path,
|
||||
backup_time: i64,
|
||||
) -> Result<(PathBuf, PathBuf, PathBuf)> {
|
||||
let folder = PathBuf::from(folder);
|
||||
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
|
||||
// Don't change this file name format, in has_backup() we use string comparison to determine which backup is newer:
|
||||
.format("delta-chat-backup-%Y-%m-%d")
|
||||
.to_string();
|
||||
|
||||
// 64 backup files per day should be enough for everyone
|
||||
for i in 0..64 {
|
||||
let mut tempdbfile = folder.clone();
|
||||
tempdbfile.push(format!("{}-{:02}.db", stem, i));
|
||||
|
||||
let mut tempfile = folder.clone();
|
||||
tempfile.push(format!("{}-{:02}.tar.part", stem, i));
|
||||
|
||||
let mut destfile = folder.clone();
|
||||
destfile.push(format!("{}-{:02}.tar", stem, i));
|
||||
|
||||
if !tempdbfile.exists().await && !tempfile.exists().await && !destfile.exists().await {
|
||||
return Ok((tempdbfile, tempfile, destfile));
|
||||
}
|
||||
}
|
||||
bail!("could not create backup file, disk full?");
|
||||
}
|
||||
|
||||
async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
|
||||
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
|
||||
let now = time();
|
||||
let (temp_path, dest_path) = get_next_backup_path(dir, now).await?;
|
||||
let _d = DeleteOnDrop(temp_path.clone());
|
||||
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, now).await?;
|
||||
let _d1 = DeleteOnDrop(temp_db_path.clone());
|
||||
let _d2 = DeleteOnDrop(temp_path.clone());
|
||||
|
||||
context
|
||||
.sql
|
||||
@@ -682,16 +560,14 @@ async fn export_backup(context: &Context, dir: &Path) -> Result<()> {
|
||||
.sql
|
||||
.execute("VACUUM;", paramsv![])
|
||||
.await
|
||||
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e));
|
||||
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e))
|
||||
.ok();
|
||||
|
||||
ensure!(
|
||||
!context.scheduler.read().await.is_running(),
|
||||
"cannot export backup, IO already running"
|
||||
);
|
||||
|
||||
// we close the database during the export
|
||||
context.sql.close().await;
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Backup '{}' to '{}'.",
|
||||
@@ -699,10 +575,13 @@ async fn export_backup(context: &Context, dir: &Path) -> Result<()> {
|
||||
dest_path.display(),
|
||||
);
|
||||
|
||||
let res = export_backup_inner(context, &temp_path).await;
|
||||
context
|
||||
.sql
|
||||
.export(&temp_db_path, passphrase)
|
||||
.await
|
||||
.with_context(|| format!("failed to backup plaintext database to {:?}", temp_db_path))?;
|
||||
|
||||
// we re-open the database after export is finished
|
||||
context.sql.open(context, context.get_dbfile(), false).await;
|
||||
let res = export_backup_inner(context, &temp_db_path, &temp_path).await;
|
||||
|
||||
match &res {
|
||||
Ok(_) => {
|
||||
@@ -721,18 +600,21 @@ impl Drop for DeleteOnDrop {
|
||||
fn drop(&mut self) {
|
||||
let file = self.0.clone();
|
||||
// Not using dc_delete_file() here because it would send a DeletedBlobFile event
|
||||
async_std::task::block_on(async move { fs::remove_file(file).await.ok() });
|
||||
async_std::task::block_on(fs::remove_file(file)).ok();
|
||||
}
|
||||
}
|
||||
|
||||
async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<()> {
|
||||
async fn export_backup_inner(
|
||||
context: &Context,
|
||||
temp_db_path: &Path,
|
||||
temp_path: &Path,
|
||||
) -> Result<()> {
|
||||
let file = File::create(temp_path).await?;
|
||||
|
||||
let mut builder = async_tar::Builder::new(file);
|
||||
|
||||
// append_path_with_name() wants the source path as the first argument, append_dir_all() wants it as the second argument.
|
||||
builder
|
||||
.append_path_with_name(context.get_dbfile(), DBFILE_BACKUP_NAME)
|
||||
.append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
|
||||
.await?;
|
||||
|
||||
let read_dir: Vec<_> = fs::read_dir(context.get_blobdir()).await?.collect().await;
|
||||
@@ -1011,16 +893,64 @@ mod tests {
|
||||
async fn test_export_and_import_key() {
|
||||
let context = TestContext::new_alice().await;
|
||||
let blobdir = context.ctx.get_blobdir();
|
||||
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir).await {
|
||||
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir, None).await {
|
||||
panic!("got error on export: {:?}", err);
|
||||
}
|
||||
|
||||
let context2 = TestContext::new_alice().await;
|
||||
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir).await {
|
||||
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir, None).await {
|
||||
panic!("got error on import: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_export_and_import_backup() -> Result<()> {
|
||||
let backup_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let context1 = TestContext::new_alice().await;
|
||||
assert!(context1.is_configured().await?);
|
||||
|
||||
let context2 = TestContext::new().await;
|
||||
assert!(!context2.is_configured().await?);
|
||||
assert!(has_backup(&context2, backup_dir.path().as_ref())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// export from context1
|
||||
assert!(imex(
|
||||
&context1,
|
||||
ImexMode::ExportBackup,
|
||||
backup_dir.path().as_ref(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.is_ok());
|
||||
let _event = context1
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
|
||||
// import to context2
|
||||
let backup = has_backup(&context2, backup_dir.path().as_ref()).await?;
|
||||
assert!(
|
||||
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
let _event = context2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
|
||||
assert!(context2.is_configured().await?);
|
||||
assert_eq!(
|
||||
context2.get_config(Config::Addr).await?,
|
||||
Some("alice@example.org".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_setup_code() {
|
||||
let norm = normalize_setup_code("123422343234423452346234723482349234");
|
||||
|
||||
459
src/job.rs
459
src/job.rs
@@ -3,28 +3,24 @@
|
||||
//! This module implements a job queue maintained in the SQLite database
|
||||
//! and job types.
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
|
||||
use async_smtp::smtp::response::{Category, Code, Detail};
|
||||
use anyhow::{bail, format_err, Context as _, Error, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::contact::{normalize_name, Contact, Modifier, Origin};
|
||||
use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_delete_file, dc_read_file, time};
|
||||
use crate::dc_tools::time;
|
||||
use crate::events::EventType;
|
||||
use crate::imap::{Imap, ImapActionResult};
|
||||
use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{self, Message, MessageState, MsgId};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::smtp::Smtp;
|
||||
use crate::smtp::{smtp_send, Smtp};
|
||||
use crate::sql;
|
||||
|
||||
// results in ~3 weeks for the last backoff timespan
|
||||
@@ -109,7 +105,6 @@ pub enum Action {
|
||||
MaybeSendLocations = 5005, // low priority ...
|
||||
MaybeSendLocationsEnded = 5007,
|
||||
SendMdn = 5010,
|
||||
SendMsgToSmtp = 5901, // ... high priority
|
||||
}
|
||||
|
||||
impl Default for Action {
|
||||
@@ -135,7 +130,6 @@ impl From<Action> for Thread {
|
||||
MaybeSendLocations => Thread::Smtp,
|
||||
MaybeSendLocationsEnded => Thread::Smtp,
|
||||
SendMdn => Thread::Smtp,
|
||||
SendMsgToSmtp => Thread::Smtp,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +197,7 @@ impl Job {
|
||||
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
|
||||
paramsv![
|
||||
self.desired_timestamp,
|
||||
self.tries as i64,
|
||||
i64::from(self.tries),
|
||||
self.param.to_string(),
|
||||
self.job_id as i32,
|
||||
],
|
||||
@@ -226,222 +220,11 @@ impl Job {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn smtp_send<F, Fut>(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
recipients: Vec<async_smtp::EmailAddress>,
|
||||
message: Vec<u8>,
|
||||
job_id: u32,
|
||||
smtp: &mut Smtp,
|
||||
success_cb: F,
|
||||
) -> Status
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = Result<()>>,
|
||||
{
|
||||
// hold the smtp lock during sending of a job and
|
||||
// its ok/error response processing. Note that if a message
|
||||
// was sent we need to mark it in the database ASAP as we
|
||||
// otherwise might send it twice.
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(context, "smtp-sending out mime message:");
|
||||
println!("{}", String::from_utf8_lossy(&message));
|
||||
}
|
||||
|
||||
smtp.connectivity.set_working(context).await;
|
||||
|
||||
let send_result = smtp.send(context, recipients, message, job_id).await;
|
||||
smtp.last_send_error = send_result.as_ref().err().map(|e| e.to_string());
|
||||
|
||||
let status = match send_result {
|
||||
Err(crate::smtp::send::Error::SmtpSend(err)) => {
|
||||
// Remote error, retry later.
|
||||
warn!(context, "SMTP failed to send: {:?}", &err);
|
||||
|
||||
let res = match err {
|
||||
async_smtp::smtp::error::Error::Permanent(ref response) => {
|
||||
// Workaround for incorrectly configured servers returning permanent errors
|
||||
// instead of temporary ones.
|
||||
let maybe_transient = match response.code {
|
||||
// Sometimes servers send a permanent error when actually it is a temporary error
|
||||
// For documentation see <https://tools.ietf.org/html/rfc3463>
|
||||
Code {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
..
|
||||
} => {
|
||||
// Ignore status code 5.5.0, see <https://support.delta.chat/t/every-other-message-gets-stuck/877/2>
|
||||
// Maybe incorrectly configured Postfix milter with "reject" instead of "tempfail", which returns
|
||||
// "550 5.5.0 Service unavailable" instead of "451 4.7.1 Service unavailable - try again later".
|
||||
//
|
||||
// Other enhanced status codes, such as Postfix
|
||||
// "550 5.1.1 <foobar@example.org>: Recipient address rejected: User unknown in local recipient table"
|
||||
// are not ignored.
|
||||
response.first_word() == Some(&"5.5.0".to_string())
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if maybe_transient {
|
||||
Status::RetryLater
|
||||
} else {
|
||||
// If we do not retry, add an info message to the chat.
|
||||
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
|
||||
// should definitely go here, because user has to open the link to
|
||||
// resume message sending.
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
}
|
||||
}
|
||||
async_smtp::smtp::error::Error::Transient(ref response) => {
|
||||
// We got a transient 4xx response from SMTP server.
|
||||
// Give some time until the server-side error maybe goes away.
|
||||
|
||||
if let Some(first_word) = response.first_word() {
|
||||
if first_word.ends_with(".1.1")
|
||||
|| first_word.ends_with(".1.2")
|
||||
|| first_word.ends_with(".1.3")
|
||||
{
|
||||
// Sometimes we receive transient errors that should be permanent.
|
||||
// Any extended smtp status codes like x.1.1, x.1.2 or x.1.3 that we
|
||||
// receive as a transient error are misconfigurations of the smtp server.
|
||||
// See <https://tools.ietf.org/html/rfc3463#section-3.2>
|
||||
info!(context, "Smtp-job #{} Received extended status code {} for a transient error. This looks like a misconfigured smtp server, let's fail immediatly", self.job_id, first_word);
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
} else {
|
||||
Status::RetryLater
|
||||
}
|
||||
} else {
|
||||
Status::RetryLater
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if smtp.has_maybe_stale_connection().await {
|
||||
info!(context, "stale connection? immediately reconnecting");
|
||||
Status::RetryNow
|
||||
} else {
|
||||
Status::RetryLater
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// this clears last_success info
|
||||
smtp.disconnect().await;
|
||||
|
||||
res
|
||||
}
|
||||
Err(crate::smtp::send::Error::Envelope(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect().await;
|
||||
warn!(context, "SMTP job is invalid: {}", err);
|
||||
Status::Finished(Err(err.into()))
|
||||
}
|
||||
Err(crate::smtp::send::Error::NoTransport) => {
|
||||
// Should never happen.
|
||||
// It does not even make sense to disconnect here.
|
||||
error!(context, "SMTP job failed because SMTP has no transport");
|
||||
Status::Finished(Err(format_err!("SMTP has not transport")))
|
||||
}
|
||||
Err(crate::smtp::send::Error::Other(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect().await;
|
||||
warn!(context, "unable to load job: {}", err);
|
||||
Status::Finished(Err(err))
|
||||
}
|
||||
Ok(()) => {
|
||||
job_try!(success_cb().await);
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
};
|
||||
|
||||
if let Status::Finished(Err(err)) = &status {
|
||||
// We couldn't send the message, so mark it as failed
|
||||
let msg_id = MsgId::new(self.foreign_id);
|
||||
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
|
||||
}
|
||||
status
|
||||
}
|
||||
|
||||
pub(crate) async fn send_msg_to_smtp(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
|
||||
// SMTP server, if not yet done
|
||||
if let Err(err) = smtp.connect_configured(context).await {
|
||||
warn!(context, "SMTP connection failure: {:?}", err);
|
||||
smtp.last_send_error = Some(format!("SMTP connection failure: {:#}", err));
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
let filename = job_try!(job_try!(self
|
||||
.param
|
||||
.get_path(Param::File, context)
|
||||
.map_err(|_| format_err!("Can't get filename")))
|
||||
.ok_or_else(|| format_err!("Can't get filename")));
|
||||
let body = job_try!(dc_read_file(context, &filename).await);
|
||||
let recipients = job_try!(self.param.get(Param::Recipients).ok_or_else(|| {
|
||||
warn!(context, "Missing recipients for job {}", self.job_id);
|
||||
format_err!("Missing recipients")
|
||||
}));
|
||||
|
||||
let recipients_list = recipients
|
||||
.split('\x1e')
|
||||
.filter_map(
|
||||
|addr| match async_smtp::EmailAddress::new(addr.to_string()) {
|
||||
Ok(addr) => Some(addr),
|
||||
Err(err) => {
|
||||
warn!(context, "invalid recipient: {} {:?}", addr, err);
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
/* if there is a msg-id and it does not exist in the db, cancel sending.
|
||||
this happends if dc_delete_msgs() was called
|
||||
before the generated mime was sent out */
|
||||
if 0 != self.foreign_id {
|
||||
match message::exists(context, MsgId::new(self.foreign_id)).await {
|
||||
Ok(exists) => {
|
||||
if !exists {
|
||||
return Status::Finished(Err(format_err!(
|
||||
"Not sending Message {} as it was deleted",
|
||||
self.foreign_id
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "failed to check message existence: {:?}", err);
|
||||
smtp.last_send_error =
|
||||
Some(format!("failed to check message existence: {:#}", err));
|
||||
return Status::RetryLater;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let foreign_id = self.foreign_id;
|
||||
self.smtp_send(context, recipients_list, body, self.job_id, smtp, || {
|
||||
async move {
|
||||
// smtp success, update db ASAP, then delete smtp file
|
||||
if 0 != foreign_id {
|
||||
set_delivered(context, MsgId::new(foreign_id)).await?;
|
||||
}
|
||||
// now also delete the generated file
|
||||
dc_delete_file(context, filename).await;
|
||||
|
||||
// finally, create another send-job if there are items to be synced.
|
||||
// triggering sync-job after msg-send-job guarantees, the recipient has grpid etc.
|
||||
// once the sync message arrives.
|
||||
// if there are no items to sync, this function returns fast.
|
||||
context.send_sync_msg().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get `SendMdn` jobs with foreign_id equal to `contact_id` excluding the `job_id` job.
|
||||
async fn get_additional_mdn_jobs(
|
||||
&self,
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
) -> Result<(Vec<u32>, Vec<String>)> {
|
||||
// Extract message IDs from job parameters
|
||||
let res: Vec<(u32, MsgId)> = context
|
||||
@@ -488,7 +271,7 @@ impl Job {
|
||||
return Status::Finished(Err(format_err!("MDNs are disabled")));
|
||||
}
|
||||
|
||||
let contact_id = self.foreign_id;
|
||||
let contact_id = ContactId::new(self.foreign_id);
|
||||
let contact = job_try!(Contact::load_from_db(context, contact_id).await);
|
||||
if contact.is_blocked() {
|
||||
return Status::Finished(Err(format_err!("Contact is blocked")));
|
||||
@@ -535,14 +318,13 @@ impl Job {
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
self.smtp_send(context, recipients, body, self.job_id, smtp, || {
|
||||
async move {
|
||||
// Remove additional SendMdn jobs we have aggregated into this one.
|
||||
kill_ids(context, &additional_job_ids).await?;
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.await
|
||||
let status = smtp_send(context, &recipients, &body, smtp, msg_id, 0).await;
|
||||
if matches!(status, Status::Finished(Ok(_))) {
|
||||
// Remove additional SendMdn jobs we have aggregated into this one.
|
||||
job_try!(kill_ids(context, &additional_job_ids).await);
|
||||
}
|
||||
|
||||
status
|
||||
}
|
||||
|
||||
/// Read the recipients from old emails sent by the user and add them as contacts.
|
||||
@@ -583,37 +365,38 @@ impl Job {
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
/// Synchronizes UIDs for sentbox, inbox and mvbox, in this order.
|
||||
///
|
||||
/// If a copy of the message is present in multiple folders, mvbox
|
||||
/// is preferred to inbox, which is in turn preferred to
|
||||
/// sentbox. This is because in the database it is impossible to
|
||||
/// store multiple UIDs for one message, so we prefer to
|
||||
/// automatically delete messages in the folders managed by Delta
|
||||
/// Chat in contrast to the Sent folder, which is normally managed
|
||||
/// by the user via webmail or another email client.
|
||||
/// Synchronizes UIDs for all folders.
|
||||
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
let sentbox_folder = job_try!(context.get_config(Config::ConfiguredSentboxFolder).await);
|
||||
if let Some(sentbox_folder) = sentbox_folder {
|
||||
job_try!(imap.resync_folder_uids(context, sentbox_folder).await);
|
||||
let all_folders = match imap.list_folders(context).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
warn!(context, "Listing folders for resync failed: {:#}", e);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
};
|
||||
|
||||
let mut any_failed = false;
|
||||
|
||||
for folder in all_folders {
|
||||
if let Err(e) = imap
|
||||
.resync_folder_uids(context, folder.name().to_string())
|
||||
.await
|
||||
{
|
||||
warn!(context, "{:#}", e);
|
||||
any_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
let inbox_folder = job_try!(context.get_config(Config::ConfiguredInboxFolder).await);
|
||||
if let Some(inbox_folder) = inbox_folder {
|
||||
job_try!(imap.resync_folder_uids(context, inbox_folder).await);
|
||||
if any_failed {
|
||||
Status::RetryLater
|
||||
} else {
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
let mvbox_folder = job_try!(context.get_config(Config::ConfiguredMvboxFolder).await);
|
||||
if let Some(mvbox_folder) = mvbox_folder {
|
||||
job_try!(imap.resync_folder_uids(context, mvbox_folder).await);
|
||||
}
|
||||
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
async fn markseen_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
@@ -689,9 +472,12 @@ pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
|
||||
|
||||
/// Remove jobs with specified IDs.
|
||||
async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
|
||||
if job_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let q = format!(
|
||||
"DELETE FROM jobs WHERE id IN({})",
|
||||
job_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
|
||||
sql::repeat_vars(job_ids.len())?
|
||||
);
|
||||
context
|
||||
.sql
|
||||
@@ -711,17 +497,6 @@ pub async fn action_exists(context: &Context, action: Action) -> Result<bool> {
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn set_delivered(context: &Context, msg_id: MsgId) -> Result<()> {
|
||||
message::update_msg_state(context, msg_id, MessageState::OutDelivered).await;
|
||||
let chat_id: ChatId = context
|
||||
.sql
|
||||
.query_get_value("SELECT chat_id FROM msgs WHERE id=?", paramsv![msg_id])
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
context.emit_event(EventType::MsgDelivered { chat_id, msg_id });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) {
|
||||
let mailbox = if let Ok(Some(m)) = context.get_config(folder).await {
|
||||
m
|
||||
@@ -767,132 +542,6 @@ async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, fold
|
||||
};
|
||||
}
|
||||
|
||||
/// Constructs a job for sending a message.
|
||||
///
|
||||
/// Returns `None` if no messages need to be sent out.
|
||||
///
|
||||
/// In order to be processed, must be `add`ded.
|
||||
pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job>> {
|
||||
let mut msg = Message::load_from_db(context, msg_id).await?;
|
||||
msg.try_calc_and_set_dimensions(context).await.ok();
|
||||
|
||||
/* create message */
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
|
||||
let attach_selfavatar = match chat::shall_attach_selfavatar(context, msg.chat_id).await {
|
||||
Ok(attach_selfavatar) => attach_selfavatar,
|
||||
Err(err) => {
|
||||
warn!(context, "job: cannot get selfavatar-state: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar).await?;
|
||||
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
let from = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
// Send BCC to self if it is enabled and we are not going to
|
||||
// delete it immediately.
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
&& context.get_config_delete_server_after().await? != Some(0)
|
||||
&& !recipients
|
||||
.iter()
|
||||
.any(|x| x.to_lowercase() == lowercase_from)
|
||||
{
|
||||
recipients.push(from);
|
||||
}
|
||||
|
||||
if recipients.is_empty() {
|
||||
// may happen eg. for groups with only SELF and bcc_self disabled
|
||||
info!(
|
||||
context,
|
||||
"message {} has no recipient, skipping smtp-send", msg_id
|
||||
);
|
||||
set_delivered(context, msg_id).await?;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let rendered_msg = match mimefactory.render(context).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
|
||||
Err(err)
|
||||
}
|
||||
}?;
|
||||
|
||||
if needs_encryption && !rendered_msg.is_encrypted {
|
||||
/* unrecoverable */
|
||||
message::set_msg_failed(
|
||||
context,
|
||||
msg_id,
|
||||
Some("End-to-end-encryption unavailable unexpectedly."),
|
||||
)
|
||||
.await;
|
||||
bail!(
|
||||
"e2e encryption unavailable {} - {:?}",
|
||||
msg_id,
|
||||
needs_encryption
|
||||
);
|
||||
}
|
||||
|
||||
if rendered_msg.is_gossiped {
|
||||
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
|
||||
}
|
||||
|
||||
if 0 != rendered_msg.last_added_location_id {
|
||||
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
|
||||
error!(context, "Failed to set kml sent_timestamp: {:?}", err);
|
||||
}
|
||||
if !msg.hidden {
|
||||
if let Err(err) =
|
||||
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
|
||||
.await
|
||||
{
|
||||
error!(context, "Failed to set msg_location_id: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||
if let Err(err) = context.delete_sync_ids(sync_ids).await {
|
||||
error!(context, "Failed to delete sync ids: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if attach_selfavatar {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
|
||||
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if rendered_msg.is_encrypted && !needs_encryption {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
msg.update_param(context).await;
|
||||
}
|
||||
|
||||
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
|
||||
let mut param = Params::new();
|
||||
let bytes = &rendered_msg.message;
|
||||
let blob = BlobObject::create(context, &rendered_msg.rfc724_mid, bytes).await?;
|
||||
|
||||
let recipients = recipients.join("\x1e");
|
||||
param.set(Param::File, blob.as_name());
|
||||
param.set(Param::Recipients, &recipients);
|
||||
|
||||
msg.subject = rendered_msg.subject.clone();
|
||||
msg.update_subject(context).await;
|
||||
|
||||
let job = create(Action::SendMsgToSmtp, msg_id.to_u32(), param, 0)?;
|
||||
|
||||
Ok(Some(job))
|
||||
}
|
||||
|
||||
pub(crate) enum Connection<'a> {
|
||||
Inbox(&'a mut Imap),
|
||||
Smtp(&'a mut Smtp),
|
||||
@@ -1000,7 +649,6 @@ async fn perform_job_action(
|
||||
|
||||
let try_res = match job.action {
|
||||
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
|
||||
Action::SendMsgToSmtp => job.send_msg_to_smtp(context, connection.smtp()).await,
|
||||
Action::SendMdn => job.send_mdn(context, connection.smtp()).await,
|
||||
Action::MaybeSendLocations => location::job_maybe_send_locations(context, job).await,
|
||||
Action::MaybeSendLocationsEnded => {
|
||||
@@ -1040,7 +688,7 @@ fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
|
||||
if seconds < 1 {
|
||||
seconds = 1;
|
||||
}
|
||||
seconds as i64
|
||||
i64::from(seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1049,7 +697,11 @@ async fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
|
||||
let mut param = Params::new();
|
||||
param.set(Param::MsgId, msg.id.to_u32().to_string());
|
||||
|
||||
add(context, Job::new(Action::SendMdn, msg.from_id, param, 0)).await?;
|
||||
add(
|
||||
context,
|
||||
Job::new(Action::SendMdn, msg.from_id.to_u32(), param, 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1064,16 +716,6 @@ pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a job.
|
||||
pub fn create(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Result<Job> {
|
||||
ensure!(
|
||||
action != Action::Unknown,
|
||||
"Invalid action passed to job_add"
|
||||
);
|
||||
|
||||
Ok(Job::new(action, foreign_id, param, delay_seconds))
|
||||
}
|
||||
|
||||
/// Adds a job to the database, scheduling it.
|
||||
pub async fn add(context: &Context, job: Job) -> Result<()> {
|
||||
let action = job.action;
|
||||
@@ -1092,10 +734,7 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
|
||||
info!(context, "interrupt: imap");
|
||||
context.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
}
|
||||
Action::MaybeSendLocations
|
||||
| Action::MaybeSendLocationsEnded
|
||||
| Action::SendMdn
|
||||
| Action::SendMsgToSmtp => {
|
||||
Action::MaybeSendLocations | Action::MaybeSendLocationsEnded | Action::SendMdn => {
|
||||
info!(context, "interrupt: smtp");
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
}
|
||||
|
||||
19
src/key.rs
19
src/key.rs
@@ -4,7 +4,7 @@ use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{format_err, Result};
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
@@ -50,8 +50,7 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
|
||||
/// the ASCII-armored representation.
|
||||
fn from_asc(data: &str) -> Result<(Self::KeyType, BTreeMap<String, String>)> {
|
||||
let bytes = data.as_bytes();
|
||||
Self::KeyType::from_armor_single(Cursor::new(bytes))
|
||||
.map_err(|err| format_err!("rPGP error: {}", err))
|
||||
Self::KeyType::from_armor_single(Cursor::new(bytes)).context("rPGP error")
|
||||
}
|
||||
|
||||
/// Load the users' default key from the database.
|
||||
@@ -202,7 +201,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
let addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("No address configured"))?;
|
||||
.context("no address configured")?;
|
||||
let addr = EmailAddress::new(&addr)?;
|
||||
let _guard = context.generating_key_mutex.lock().await;
|
||||
|
||||
@@ -289,17 +288,17 @@ pub async fn store_self_keypair(
|
||||
paramsv![public_key, secret_key],
|
||||
)
|
||||
.await
|
||||
.map_err(|err| err.context("failed to remove old use of key"))?;
|
||||
.context("failed to remove old use of key")?;
|
||||
if default == KeyPairUse::Default {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
|
||||
.await
|
||||
.map_err(|err| err.context("failed to clear default"))?;
|
||||
.context("failed to clear default")?;
|
||||
}
|
||||
let is_default = match default {
|
||||
KeyPairUse::Default => true as i32,
|
||||
KeyPairUse::ReadOnly => false as i32,
|
||||
KeyPairUse::Default => i32::from(true),
|
||||
KeyPairUse::ReadOnly => i32::from(false),
|
||||
};
|
||||
|
||||
let addr = keypair.addr.to_string();
|
||||
@@ -313,13 +312,13 @@ pub async fn store_self_keypair(
|
||||
paramsv![addr, is_default, public_key, secret_key, t],
|
||||
)
|
||||
.await
|
||||
.map_err(|err| err.context("failed to insert keypair"))?;
|
||||
.context("failed to insert keypair")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A key fingerprint
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Fingerprint(Vec<u8>);
|
||||
|
||||
impl Fingerprint {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
clippy::all,
|
||||
clippy::indexing_slicing,
|
||||
clippy::wildcard_imports,
|
||||
clippy::needless_borrow
|
||||
clippy::needless_borrow,
|
||||
clippy::cast_lossless
|
||||
)]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
@@ -87,6 +88,7 @@ pub mod stock_str;
|
||||
mod sync;
|
||||
mod token;
|
||||
mod update_helper;
|
||||
pub mod webxdc;
|
||||
#[macro_use]
|
||||
mod dehtml;
|
||||
mod color;
|
||||
|
||||
@@ -7,12 +7,13 @@ use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::constants::DC_CONTACT_ID_SELF;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::events::EventType;
|
||||
use crate::job::{self, Job};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Params;
|
||||
use crate::stock_str;
|
||||
@@ -25,7 +26,7 @@ pub struct Location {
|
||||
pub longitude: f64,
|
||||
pub accuracy: f64,
|
||||
pub timestamp: i64,
|
||||
pub contact_id: u32,
|
||||
pub contact_id: ContactId,
|
||||
pub msg_id: u32,
|
||||
pub chat_id: ChatId,
|
||||
pub marker: Option<String>,
|
||||
@@ -101,10 +102,10 @@ impl Kml {
|
||||
let val = event.unescape_and_decode(reader).unwrap_or_default();
|
||||
|
||||
let val = val
|
||||
.replace("\n", "")
|
||||
.replace("\r", "")
|
||||
.replace("\t", "")
|
||||
.replace(" ", "");
|
||||
.replace('\n', "")
|
||||
.replace('\r', "")
|
||||
.replace('\t', "")
|
||||
.replace(' ', "");
|
||||
|
||||
if self.tag.contains(KmlTag::WHEN) && val.len() >= 19 {
|
||||
// YYYY-MM-DDTHH:MM:SSZ
|
||||
@@ -558,7 +559,7 @@ pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id:
|
||||
pub(crate) async fn save(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
locations: &[Location],
|
||||
independent: bool,
|
||||
) -> Result<Option<u32>> {
|
||||
@@ -585,12 +586,12 @@ pub(crate) async fn save(
|
||||
conn.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
|
||||
let mut stmt_insert = conn.prepare_cached(stmt_insert)?;
|
||||
|
||||
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
|
||||
let exists = stmt_test.exists(paramsv![timestamp, contact_id])?;
|
||||
|
||||
if independent || !exists {
|
||||
stmt_insert.execute(paramsv![
|
||||
timestamp,
|
||||
contact_id as i32,
|
||||
contact_id,
|
||||
chat_id,
|
||||
latitude,
|
||||
longitude,
|
||||
|
||||
@@ -253,7 +253,8 @@ impl LoginParam {
|
||||
sql.set_raw_config(key, Some(&self.imap.server)).await?;
|
||||
|
||||
let key = format!("{}mail_port", prefix);
|
||||
sql.set_raw_config_int(key, self.imap.port as i32).await?;
|
||||
sql.set_raw_config_int(key, i32::from(self.imap.port))
|
||||
.await?;
|
||||
|
||||
let key = format!("{}mail_user", prefix);
|
||||
sql.set_raw_config(key, Some(&self.imap.user)).await?;
|
||||
@@ -273,7 +274,8 @@ impl LoginParam {
|
||||
sql.set_raw_config(key, Some(&self.smtp.server)).await?;
|
||||
|
||||
let key = format!("{}send_port", prefix);
|
||||
sql.set_raw_config_int(key, self.smtp.port as i32).await?;
|
||||
sql.set_raw_config_int(key, i32::from(self.smtp.port))
|
||||
.await?;
|
||||
|
||||
let key = format!("{}send_user", prefix);
|
||||
sql.set_raw_config(key, Some(&self.smtp.user)).await?;
|
||||
|
||||
410
src/message.rs
410
src/message.rs
@@ -1,7 +1,6 @@
|
||||
//! # Messages and their identifiers.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryInto;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use anyhow::{ensure, format_err, Context as _, Result};
|
||||
use async_std::path::{Path, PathBuf};
|
||||
@@ -11,17 +10,17 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::constants::{
|
||||
Blocked, Chattype, VideochatType, Viewtype, DC_CHAT_ID_TRASH, DC_CONTACT_ID_INFO,
|
||||
DC_CONTACT_ID_SELF, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
|
||||
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_CONTACT_ID_INFO, DC_CONTACT_ID_SELF,
|
||||
DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
|
||||
};
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{
|
||||
dc_create_smeared_timestamp, dc_get_filebytes, dc_get_filemeta, dc_gm2local_offset,
|
||||
dc_read_file, dc_timestamp_to_str, dc_truncate, time,
|
||||
};
|
||||
use crate::download::DownloadState;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::ephemeral::{start_ephemeral_timers_msgids, Timer as EphemeralTimer};
|
||||
use crate::events::EventType;
|
||||
use crate::job::{self, Action};
|
||||
use crate::log::LogExt;
|
||||
@@ -29,6 +28,7 @@ use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::split_armored_data;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::summary::Summary;
|
||||
|
||||
@@ -113,14 +113,25 @@ WHERE id=?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a message and corresponding MDNs from the database.
|
||||
/// Deletes a message, corresponding MDNs and unsent SMTP messages from the database.
|
||||
pub async fn delete_from_db(self, context: &Context) -> Result<()> {
|
||||
// We don't use transactions yet, so remove MDNs first to make
|
||||
// sure they are not left while the message is deleted.
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE msg_id=?", paramsv![self])
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsv![self])
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM msgs_status_updates WHERE msg_id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM msgs WHERE id=?;", paramsv![self])
|
||||
@@ -128,6 +139,20 @@ WHERE id=?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> {
|
||||
update_msg_state(context, self, MessageState::OutDelivered).await?;
|
||||
let chat_id: ChatId = context
|
||||
.sql
|
||||
.query_get_value("SELECT chat_id FROM msgs WHERE id=?", paramsv![self])
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
context.emit_event(EventType::MsgDelivered {
|
||||
chat_id,
|
||||
msg_id: self,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
///
|
||||
/// Avoid using this, eventually types should be cleaned up enough
|
||||
@@ -155,10 +180,10 @@ impl rusqlite::types::ToSql for MsgId {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
if self.0 <= DC_MSG_ID_LAST_SPECIAL {
|
||||
return Err(rusqlite::Error::ToSqlConversionFailure(
|
||||
format_err!("Invalid MsgId").into(),
|
||||
format_err!("Invalid MsgId {}", self.0).into(),
|
||||
));
|
||||
}
|
||||
let val = rusqlite::types::Value::Integer(self.0 as i64);
|
||||
let val = rusqlite::types::Value::Integer(i64::from(self.0));
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
@@ -169,7 +194,7 @@ impl rusqlite::types::FromSql for MsgId {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
// Would be nice if we could use match here, but alas.
|
||||
i64::column_result(value).and_then(|val| {
|
||||
if 0 <= val && val <= std::u32::MAX as i64 {
|
||||
if 0 <= val && val <= i64::from(std::u32::MAX) {
|
||||
Ok(MsgId::new(val as u32))
|
||||
} else {
|
||||
Err(rusqlite::types::FromSqlError::OutOfRange(val))
|
||||
@@ -215,8 +240,8 @@ impl Default for MessengerMessage {
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
pub(crate) id: MsgId,
|
||||
pub(crate) from_id: u32,
|
||||
pub(crate) to_id: u32,
|
||||
pub(crate) from_id: ContactId,
|
||||
pub(crate) to_id: ContactId,
|
||||
pub(crate) chat_id: ChatId,
|
||||
pub(crate) viewtype: Viewtype,
|
||||
pub(crate) state: MessageState,
|
||||
@@ -362,7 +387,7 @@ impl Message {
|
||||
}
|
||||
|
||||
pub async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
|
||||
if chat::msgtype_has_file(self.viewtype) {
|
||||
if self.viewtype.has_file() {
|
||||
let file_param = self.param.get_path(Param::File, context)?;
|
||||
if let Some(path_and_filename) = file_param {
|
||||
if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
|
||||
@@ -431,7 +456,7 @@ impl Message {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn get_from_id(&self) -> u32 {
|
||||
pub fn get_from_id(&self) -> ContactId {
|
||||
self.from_id
|
||||
}
|
||||
|
||||
@@ -595,7 +620,7 @@ impl Message {
|
||||
/// copied to the blobdir. Thus those attachments need to be
|
||||
/// created immediately in the blobdir with a valid filename.
|
||||
pub fn is_increation(&self) -> bool {
|
||||
chat::msgtype_has_file(self.viewtype) && self.state == MessageState::OutPreparing
|
||||
self.viewtype.has_file() && self.state == MessageState::OutPreparing
|
||||
}
|
||||
|
||||
pub fn is_setupmessage(&self) -> bool {
|
||||
@@ -756,36 +781,41 @@ impl Message {
|
||||
///
|
||||
/// The message itself is not required to exist in the database,
|
||||
/// it may even be deleted from the database by the time the message is prepared.
|
||||
pub async fn set_quote(&mut self, context: &Context, quote: &Message) -> Result<()> {
|
||||
ensure!(
|
||||
!quote.rfc724_mid.is_empty(),
|
||||
"Message without Message-Id cannot be quoted"
|
||||
);
|
||||
self.in_reply_to = Some(quote.rfc724_mid.clone());
|
||||
pub async fn set_quote(&mut self, context: &Context, quote: Option<&Message>) -> Result<()> {
|
||||
if let Some(quote) = quote {
|
||||
ensure!(
|
||||
!quote.rfc724_mid.is_empty(),
|
||||
"Message without Message-Id cannot be quoted"
|
||||
);
|
||||
self.in_reply_to = Some(quote.rfc724_mid.clone());
|
||||
|
||||
if quote
|
||||
.param
|
||||
.get_bool(Param::GuaranteeE2ee)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
self.param.set(Param::GuaranteeE2ee, "1");
|
||||
if quote
|
||||
.param
|
||||
.get_bool(Param::GuaranteeE2ee)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
self.param.set(Param::GuaranteeE2ee, "1");
|
||||
}
|
||||
|
||||
let text = quote.get_text().unwrap_or_default();
|
||||
self.param.set(
|
||||
Param::Quote,
|
||||
if text.is_empty() {
|
||||
// Use summary, similar to "Image" to avoid sending empty quote.
|
||||
quote
|
||||
.get_summary(context, None)
|
||||
.await?
|
||||
.truncated_text(500)
|
||||
.to_string()
|
||||
} else {
|
||||
text
|
||||
},
|
||||
);
|
||||
} else {
|
||||
self.in_reply_to = None;
|
||||
self.param.remove(Param::Quote);
|
||||
}
|
||||
|
||||
let text = quote.get_text().unwrap_or_default();
|
||||
self.param.set(
|
||||
Param::Quote,
|
||||
if text.is_empty() {
|
||||
// Use summary, similar to "Image" to avoid sending empty quote.
|
||||
quote
|
||||
.get_summary(context, None)
|
||||
.await?
|
||||
.truncated_text(500)
|
||||
.to_string()
|
||||
} else {
|
||||
text
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -795,16 +825,21 @@ impl Message {
|
||||
|
||||
pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
|
||||
if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
|
||||
if let Some(in_reply_to) = &self.in_reply_to {
|
||||
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
return if msg.chat_id.is_trash() {
|
||||
// If message is already moved to trash chat, pretend it does not exist.
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(msg))
|
||||
};
|
||||
}
|
||||
return self.parent(context).await;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
|
||||
if let Some(in_reply_to) = &self.in_reply_to {
|
||||
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
return if msg.chat_id.is_trash() {
|
||||
// If message is already moved to trash chat, pretend it does not exist.
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(msg))
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
@@ -1014,7 +1049,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
|
||||
"SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;",
|
||||
paramsv![msg_id],
|
||||
|row| {
|
||||
let contact_id: i32 = row.get(0)?;
|
||||
let contact_id: ContactId = row.get(0)?;
|
||||
let ts: i64 = row.get(1)?;
|
||||
Ok((contact_id, ts))
|
||||
},
|
||||
@@ -1026,7 +1061,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
|
||||
let fts = dc_timestamp_to_str(ts);
|
||||
ret += &format!("Read: {}", fts);
|
||||
|
||||
let name = Contact::load_from_db(context, contact_id.try_into()?)
|
||||
let name = Contact::load_from_db(context, contact_id)
|
||||
.await
|
||||
.map(|contact| contact.get_name_n_addr())
|
||||
.unwrap_or_default();
|
||||
@@ -1156,6 +1191,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
||||
"webm" => (Viewtype::Video, "video/webm"),
|
||||
"webp" => (Viewtype::Image, "image/webp"), // iOS via SDWebImage, Android since 4.0
|
||||
"wmv" => (Viewtype::Video, "video/x-ms-wmv"),
|
||||
"xdc" => (Viewtype::Webxdc, "application/webxdc+zip"),
|
||||
"xhtml" => (Viewtype::File, "application/xhtml+xml"),
|
||||
"xlsx" => (
|
||||
Viewtype::File,
|
||||
@@ -1201,7 +1237,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
for msg_id in msg_ids.iter() {
|
||||
let msg = Message::load_from_db(context, *msg_id).await?;
|
||||
if msg.location_id > 0 {
|
||||
delete_poi_location(context, msg.location_id).await;
|
||||
delete_poi_location(context, msg.location_id).await?;
|
||||
}
|
||||
msg_id
|
||||
.trash(context)
|
||||
@@ -1234,15 +1270,15 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_poi_location(context: &Context, location_id: u32) -> bool {
|
||||
async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM locations WHERE independent = 1 AND id=?;",
|
||||
paramsv![location_id as i32],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
|
||||
@@ -1250,54 +1286,56 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let conn = context.sql.get_conn().await?;
|
||||
let msgs = async_std::task::spawn_blocking(move || -> Result<_> {
|
||||
let mut stmt = conn.prepare_cached(concat!(
|
||||
"SELECT",
|
||||
" m.chat_id AS chat_id,",
|
||||
" m.state AS state,",
|
||||
" c.blocked AS blocked",
|
||||
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
|
||||
" WHERE m.id=? AND m.chat_id>9"
|
||||
))?;
|
||||
|
||||
let mut msgs = Vec::with_capacity(msg_ids.len());
|
||||
for id in msg_ids.into_iter() {
|
||||
let query_res = stmt.query_row(paramsv![id], |row| {
|
||||
let msgs = context
|
||||
.sql
|
||||
.query_map(
|
||||
format!(
|
||||
"SELECT
|
||||
m.id AS id,
|
||||
m.chat_id AS chat_id,
|
||||
m.state AS state,
|
||||
m.ephemeral_timer AS ephemeral_timer,
|
||||
c.blocked AS blocked
|
||||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
|
||||
WHERE m.id IN ({}) AND m.chat_id>9",
|
||||
sql::repeat_vars(msg_ids.len())?
|
||||
),
|
||||
rusqlite::params_from_iter(&msg_ids),
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let state: MessageState = row.get("state")?;
|
||||
let blocked: Option<Blocked> = row.get("blocked")?;
|
||||
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
|
||||
Ok((
|
||||
row.get::<_, ChatId>("chat_id")?,
|
||||
row.get::<_, MessageState>("state")?,
|
||||
row.get::<_, Option<Blocked>>("blocked")?
|
||||
.unwrap_or_default(),
|
||||
id,
|
||||
chat_id,
|
||||
state,
|
||||
blocked.unwrap_or_default(),
|
||||
ephemeral_timer,
|
||||
))
|
||||
});
|
||||
if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res {
|
||||
continue;
|
||||
}
|
||||
let (chat_id, state, blocked) = query_res.map_err(Into::<anyhow::Error>::into)?;
|
||||
msgs.push((id, chat_id, state, blocked));
|
||||
}
|
||||
drop(stmt);
|
||||
drop(conn);
|
||||
Ok(msgs)
|
||||
})
|
||||
.await?;
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut updated_chat_ids = BTreeMap::new();
|
||||
|
||||
for (id, curr_chat_id, curr_state, curr_blocked) in msgs.into_iter() {
|
||||
if let Err(err) = id.start_ephemeral_timer(context).await {
|
||||
error!(
|
||||
context,
|
||||
"Failed to start ephemeral timer for message {}: {}", id, err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if msgs
|
||||
.iter()
|
||||
.any(|(_id, _chat_id, _state, _blocked, ephemeral_timer)| {
|
||||
*ephemeral_timer != EphemeralTimer::Disabled
|
||||
})
|
||||
{
|
||||
start_ephemeral_timers_msgids(context, &msg_ids)
|
||||
.await
|
||||
.context("failed to start ephemeral timers")?;
|
||||
}
|
||||
|
||||
let mut updated_chat_ids = BTreeSet::new();
|
||||
for (id, curr_chat_id, curr_state, curr_blocked, _curr_ephemeral_timer) in msgs.into_iter() {
|
||||
if curr_blocked == Blocked::Not
|
||||
&& (curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed)
|
||||
{
|
||||
update_msg_state(context, id, MessageState::InSeen).await;
|
||||
update_msg_state(context, id, MessageState::InSeen).await?;
|
||||
info!(context, "Seen message {}.", id);
|
||||
|
||||
job::add(
|
||||
@@ -1305,26 +1343,30 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
job::Job::new(Action::MarkseenMsgOnImap, id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await?;
|
||||
updated_chat_ids.insert(curr_chat_id, true);
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
}
|
||||
}
|
||||
|
||||
for updated_chat_id in updated_chat_ids.keys() {
|
||||
context.emit_event(EventType::MsgsNoticed(*updated_chat_id));
|
||||
for updated_chat_id in updated_chat_ids {
|
||||
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageState) -> bool {
|
||||
pub(crate) async fn update_msg_state(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
state: MessageState,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=? WHERE id=?;",
|
||||
paramsv![state, msg_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// as we do not cut inside words, this results in about 32-42 characters.
|
||||
@@ -1386,7 +1428,7 @@ pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl
|
||||
/// returns Some if an event should be send
|
||||
pub async fn handle_mdn(
|
||||
context: &Context,
|
||||
from_id: u32,
|
||||
from_id: ContactId,
|
||||
rfc724_mid: &str,
|
||||
timestamp_sent: i64,
|
||||
) -> Result<Option<(ChatId, MsgId)>> {
|
||||
@@ -1439,7 +1481,7 @@ pub async fn handle_mdn(
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
|
||||
paramsv![msg_id, from_id as i32,],
|
||||
paramsv![msg_id, from_id],
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -1447,7 +1489,7 @@ pub async fn handle_mdn(
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);",
|
||||
paramsv![msg_id, from_id as i32, timestamp_sent],
|
||||
paramsv![msg_id, from_id, timestamp_sent],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1456,7 +1498,7 @@ pub async fn handle_mdn(
|
||||
|| msg_state == MessageState::OutPending
|
||||
|| msg_state == MessageState::OutDelivered
|
||||
{
|
||||
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await;
|
||||
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await?;
|
||||
Ok(Some((chat_id, msg_id)))
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -1525,9 +1567,7 @@ async fn ndn_maybe_add_info_msg(
|
||||
let contact_id =
|
||||
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
format_err!("ndn_maybe_add_info_msg: Contact ID not found")
|
||||
})?;
|
||||
.context("contact ID not found")?;
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
// Tell the user which of the recipients failed if we know that (because in
|
||||
// a group, this might otherwise be unclear)
|
||||
@@ -1663,9 +1703,106 @@ pub(crate) async fn rfc724_mid_exists(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// How a message is primarily displayed.
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Viewtype {
|
||||
Unknown = 0,
|
||||
|
||||
/// Text message.
|
||||
/// The text of the message is set using dc_msg_set_text()
|
||||
/// and retrieved with dc_msg_get_text().
|
||||
Text = 10,
|
||||
|
||||
/// Image message.
|
||||
/// If the image is an animated GIF, the type DC_MSG_GIF should be used.
|
||||
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
|
||||
/// and retrieved via dc_msg_set_file(), dc_msg_set_dimension().
|
||||
Image = 20,
|
||||
|
||||
/// Animated GIF message.
|
||||
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension()
|
||||
/// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height().
|
||||
Gif = 21,
|
||||
|
||||
/// Message containing a sticker, similar to image.
|
||||
/// If possible, the ui should display the image without borders in a transparent way.
|
||||
/// A click on a sticker will offer to install the sticker set in some future.
|
||||
Sticker = 23,
|
||||
|
||||
/// Message containing an Audio file.
|
||||
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
|
||||
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration().
|
||||
Audio = 40,
|
||||
|
||||
/// A voice message that was directly recorded by the user.
|
||||
/// For all other audio messages, the type #DC_MSG_AUDIO should be used.
|
||||
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
|
||||
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration()
|
||||
Voice = 41,
|
||||
|
||||
/// Video messages.
|
||||
/// File, width, height and durarion
|
||||
/// are set via dc_msg_set_file(), dc_msg_set_dimension(), dc_msg_set_duration()
|
||||
/// and retrieved via
|
||||
/// dc_msg_get_file(), dc_msg_get_width(),
|
||||
/// dc_msg_get_height(), dc_msg_get_duration().
|
||||
Video = 50,
|
||||
|
||||
/// Message containing any file, eg. a PDF.
|
||||
/// The file is set via dc_msg_set_file()
|
||||
/// and retrieved via dc_msg_get_file().
|
||||
File = 60,
|
||||
|
||||
/// Message is an invitation to a videochat.
|
||||
VideochatInvitation = 70,
|
||||
|
||||
/// Message is an webxdc instance.
|
||||
Webxdc = 80,
|
||||
}
|
||||
|
||||
impl Default for Viewtype {
|
||||
fn default() -> Self {
|
||||
Viewtype::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
impl Viewtype {
|
||||
/// Whether a message with this [`Viewtype`] should have a file attachment.
|
||||
pub fn has_file(&self) -> bool {
|
||||
match self {
|
||||
Viewtype::Unknown => false,
|
||||
Viewtype::Text => false,
|
||||
Viewtype::Image => true,
|
||||
Viewtype::Gif => true,
|
||||
Viewtype::Sticker => true,
|
||||
Viewtype::Audio => true,
|
||||
Viewtype::Voice => true,
|
||||
Viewtype::Video => true,
|
||||
Viewtype::File => true,
|
||||
Viewtype::VideochatInvitation => false,
|
||||
Viewtype::Webxdc => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::chat::{marknoticed_chat, ChatItem};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::DC_CONTACT_ID_DEVICE;
|
||||
@@ -1673,12 +1810,22 @@ mod tests {
|
||||
use crate::test_utils as test;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_guess_msgtype_from_suffix() {
|
||||
assert_eq!(
|
||||
guess_msgtype_from_suffix(Path::new("foo/bar-sth.mp3")),
|
||||
Some((Viewtype::Audio, "audio/mpeg"))
|
||||
);
|
||||
assert_eq!(
|
||||
guess_msgtype_from_suffix(Path::new("foo/file.html")),
|
||||
Some((Viewtype::File, "text/html"))
|
||||
);
|
||||
assert_eq!(
|
||||
guess_msgtype_from_suffix(Path::new("foo/file.xdc")),
|
||||
Some((Viewtype::Webxdc, "application/webxdc+zip"))
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -1846,7 +1993,9 @@ mod tests {
|
||||
assert!(!msg.rfc724_mid.is_empty());
|
||||
|
||||
let mut msg2 = Message::new(Viewtype::Text);
|
||||
msg2.set_quote(ctx, &msg).await.expect("can't set quote");
|
||||
msg2.set_quote(ctx, Some(&msg))
|
||||
.await
|
||||
.expect("can't set quote");
|
||||
assert!(msg2.quoted_text() == msg.get_text());
|
||||
|
||||
let quoted_msg = msg2
|
||||
@@ -1977,9 +2126,9 @@ mod tests {
|
||||
let msg2 = alice.get_last_msg().await;
|
||||
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0), alice_chat.id);
|
||||
assert_eq!(chats.get_chat_id(0), msg1.chat_id);
|
||||
assert_eq!(chats.get_chat_id(0), msg2.chat_id);
|
||||
assert_eq!(chats.get_chat_id(0)?, alice_chat.id);
|
||||
assert_eq!(chats.get_chat_id(0)?, msg1.chat_id);
|
||||
assert_eq!(chats.get_chat_id(0)?, msg2.chat_id);
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 2);
|
||||
|
||||
@@ -2043,7 +2192,7 @@ mod tests {
|
||||
let payload = alice.pop_sent_msg().await;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutDelivered).await;
|
||||
|
||||
update_msg_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await;
|
||||
update_msg_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await?;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await;
|
||||
|
||||
set_msg_failed(&alice, alice_msg.id, Some("badly failed")).await;
|
||||
@@ -2107,4 +2256,29 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_viewtype_derive_display_works_as_expected() {
|
||||
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_viewtype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::default());
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
|
||||
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
|
||||
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
|
||||
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
|
||||
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
|
||||
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
|
||||
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
|
||||
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
|
||||
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
|
||||
assert_eq!(
|
||||
Viewtype::VideochatInvitation,
|
||||
Viewtype::from_i32(70).unwrap()
|
||||
);
|
||||
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use chrono::TimeZone;
|
||||
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, Chat};
|
||||
use crate::chat::Chat;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Chattype, Viewtype, DC_FROM_HANDSHAKE};
|
||||
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
||||
use crate::contact::Contact;
|
||||
use crate::context::{get_version_str, Context};
|
||||
use crate::dc_tools::IsNoneOrEmpty;
|
||||
@@ -22,7 +22,7 @@ use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::format_flowed::{format_flowed, format_flowed_quote};
|
||||
use crate::html::new_html_mimepart;
|
||||
use crate::location;
|
||||
use crate::message::{self, Message, MsgId};
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
@@ -81,7 +81,7 @@ pub struct MimeFactory<'a> {
|
||||
/// Result of rendering a message, ready to be submitted to a send job.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderedEmail {
|
||||
pub message: Vec<u8>,
|
||||
pub message: String,
|
||||
// pub envelope: Envelope,
|
||||
pub is_encrypted: bool,
|
||||
pub is_gossiped: bool,
|
||||
@@ -207,7 +207,6 @@ impl<'a> MimeFactory<'a> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let default_str = stock_str::status_line(context).await;
|
||||
let factory = MimeFactory {
|
||||
from_addr,
|
||||
from_displayname,
|
||||
@@ -215,7 +214,7 @@ impl<'a> MimeFactory<'a> {
|
||||
selfstatus: context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await?
|
||||
.unwrap_or(default_str),
|
||||
.unwrap_or_default(),
|
||||
recipients,
|
||||
timestamp: msg.timestamp_sort,
|
||||
loaded: Loaded::Message { chat },
|
||||
@@ -246,11 +245,10 @@ impl<'a> MimeFactory<'a> {
|
||||
.get_config(Config::Displayname)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let default_str = stock_str::status_line(context).await;
|
||||
let selfstatus = context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await?
|
||||
.unwrap_or(default_str);
|
||||
.unwrap_or_default();
|
||||
let timestamp = dc_create_smeared_timestamp(context).await;
|
||||
|
||||
let res = MimeFactory::<'a> {
|
||||
@@ -283,7 +281,7 @@ impl<'a> MimeFactory<'a> {
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("Not configured"))?;
|
||||
.context("not configured")?;
|
||||
|
||||
let mut res = Vec::new();
|
||||
for (_, addr) in self
|
||||
@@ -505,33 +503,80 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Start with Internet Message Format headers in the order of the standard example
|
||||
// <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.1.1>.
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("MIME-Version".into(), "1.0".into()));
|
||||
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
|
||||
if let Some(sender_displayname) = &self.sender_displayname {
|
||||
let sender =
|
||||
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
|
||||
}
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("To".into(), to).unwrap());
|
||||
|
||||
let subject_str = self.subject_str(context).await?;
|
||||
let encoded_subject = if subject_str
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
|
||||
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
|
||||
// but we do not want to encode all subjects just because they contain a space.
|
||||
{
|
||||
subject_str.clone()
|
||||
} else {
|
||||
encode_words(&subject_str)
|
||||
};
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Subject".into(), encoded_subject));
|
||||
|
||||
let date = chrono::Utc
|
||||
.from_local_datetime(&chrono::NaiveDateTime::from_timestamp(self.timestamp, 0))
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
headers.unprotected.push(Header::new("Date".into(), date));
|
||||
|
||||
let rfc724_mid = match self.loaded {
|
||||
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
|
||||
Loaded::Mdn { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
|
||||
};
|
||||
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
|
||||
|
||||
// Amazon's SMTP servers change the `Message-ID`, just as Outlook's SMTP servers do.
|
||||
// Outlook's servers add an `X-Microsoft-Original-Message-ID` header with the original `Message-ID`,
|
||||
// and when downloading messages we look for this header in order to correctly identify
|
||||
// messages.
|
||||
// Amazon's servers do not add such a header, so we just add it ourselves.
|
||||
if let Some(server) = context.get_config(Config::ConfiguredSendServer).await? {
|
||||
if server.ends_with(".amazonaws.com") {
|
||||
headers.unprotected.push(Header::new(
|
||||
"X-Microsoft-Original-Message-ID".into(),
|
||||
rfc724_mid_headervalue.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("Message-ID".into(), rfc724_mid_headervalue));
|
||||
|
||||
// Reply headers as in <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.2>.
|
||||
if !self.in_reply_to.is_empty() {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
|
||||
}
|
||||
if !self.references.is_empty() {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("References".into(), self.references.clone()));
|
||||
}
|
||||
|
||||
if !self.in_reply_to.is_empty() {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
|
||||
}
|
||||
|
||||
let date = chrono::Utc
|
||||
.from_local_datetime(&chrono::NaiveDateTime::from_timestamp(self.timestamp, 0))
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
|
||||
headers.unprotected.push(Header::new("Date".into(), date));
|
||||
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
|
||||
|
||||
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
|
||||
if let Loaded::Mdn { .. } = self.loaded {
|
||||
headers.unprotected.push(Header::new(
|
||||
"Auto-Submitted".to_string(),
|
||||
@@ -544,6 +589,11 @@ impl<'a> MimeFactory<'a> {
|
||||
));
|
||||
}
|
||||
|
||||
// Non-standard headers.
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
|
||||
|
||||
if self.req_mdn {
|
||||
// we use "Chat-Disposition-Notification-To"
|
||||
// because replies to "Disposition-Notification-To" are weird in many cases
|
||||
@@ -558,21 +608,9 @@ impl<'a> MimeFactory<'a> {
|
||||
let grpimage = self.grpimage();
|
||||
let force_plaintext = self.should_force_plaintext();
|
||||
let skip_autocrypt = self.should_skip_autocrypt();
|
||||
let subject_str = self.subject_str(context).await?;
|
||||
let e2ee_guaranteed = self.is_e2ee_guaranteed();
|
||||
let encrypt_helper = EncryptHelper::new(context).await?;
|
||||
|
||||
let encoded_subject = if subject_str
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
|
||||
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
|
||||
// but we do not want to encode all subjects just because they contain a space.
|
||||
{
|
||||
subject_str.clone()
|
||||
} else {
|
||||
encode_words(&subject_str)
|
||||
};
|
||||
|
||||
if !skip_autocrypt {
|
||||
// unless determined otherwise we add the Autocrypt header
|
||||
let aheader = encrypt_helper.get_aheader().to_string();
|
||||
@@ -581,15 +619,6 @@ impl<'a> MimeFactory<'a> {
|
||||
.push(Header::new("Autocrypt".into(), aheader));
|
||||
}
|
||||
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Subject".into(), encoded_subject));
|
||||
|
||||
let rfc724_mid = match self.loaded {
|
||||
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
|
||||
Loaded::Mdn { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
|
||||
};
|
||||
|
||||
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?;
|
||||
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
|
||||
headers.protected.push(Header::new(
|
||||
@@ -598,25 +627,11 @@ impl<'a> MimeFactory<'a> {
|
||||
));
|
||||
}
|
||||
|
||||
headers.unprotected.push(Header::new(
|
||||
"Message-ID".into(),
|
||||
render_rfc724_mid(&rfc724_mid),
|
||||
));
|
||||
|
||||
// MIME header <https://datatracker.ietf.org/doc/html/rfc2045>.
|
||||
// Content-Type
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("To".into(), to).unwrap());
|
||||
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
|
||||
if let Some(sender_displayname) = &self.sender_displayname {
|
||||
let sender =
|
||||
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
|
||||
}
|
||||
.push(Header::new("MIME-Version".into(), "1.0".into()));
|
||||
|
||||
let mut is_gossiped = false;
|
||||
|
||||
@@ -643,6 +658,11 @@ impl<'a> MimeFactory<'a> {
|
||||
"Content-Type".to_string(),
|
||||
"multipart/report; report-type=multi-device-sync".to_string(),
|
||||
))
|
||||
} else if self.msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate {
|
||||
PartBuilder::new().header((
|
||||
"Content-Type".to_string(),
|
||||
"multipart/report; report-type=status-update".to_string(),
|
||||
))
|
||||
} else {
|
||||
PartBuilder::new().message_type(MimeMultipartType::Mixed)
|
||||
};
|
||||
@@ -767,7 +787,7 @@ impl<'a> MimeFactory<'a> {
|
||||
} = self;
|
||||
|
||||
Ok(RenderedEmail {
|
||||
message: outer_message.build().as_string().into_bytes(),
|
||||
message: outer_message.build().as_string(),
|
||||
// envelope: Envelope::new,
|
||||
is_encrypted,
|
||||
is_gossiped,
|
||||
@@ -917,7 +937,9 @@ impl<'a> MimeFactory<'a> {
|
||||
"ephemeral-timer-changed".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::LocationOnly | SystemMessage::MultiDeviceSync => {
|
||||
SystemMessage::LocationOnly
|
||||
| SystemMessage::MultiDeviceSync
|
||||
| SystemMessage::WebxdcStatusUpdate => {
|
||||
// This should prevent automatic replies,
|
||||
// such as non-delivery reports.
|
||||
//
|
||||
@@ -1118,7 +1140,7 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
|
||||
// add attachment part
|
||||
if chat::msgtype_has_file(self.msg.viewtype) {
|
||||
if self.msg.viewtype.has_file() {
|
||||
if !is_file_size_okay(context, self.msg).await? {
|
||||
bail!(
|
||||
"Message exceeds the recommended {} MB.",
|
||||
@@ -1154,6 +1176,16 @@ impl<'a> MimeFactory<'a> {
|
||||
let ids = self.msg.param.get(Param::Arg2).unwrap_or_default();
|
||||
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).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).await);
|
||||
}
|
||||
}
|
||||
|
||||
if self.attach_selfavatar {
|
||||
@@ -1285,7 +1317,7 @@ async fn build_body_file(
|
||||
.param
|
||||
.get_blob(Param::File, context, true)
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("msg has no filename"))?;
|
||||
.context("msg has no filename")?;
|
||||
let suffix = blob.suffix().unwrap_or("dat");
|
||||
|
||||
// Get file name to use for sending. For privacy purposes, we do
|
||||
@@ -1313,8 +1345,7 @@ async fn build_body_file(
|
||||
"video_{}.{}",
|
||||
chrono::Utc
|
||||
.timestamp(msg.timestamp_sort, 0)
|
||||
.format("%Y-%m-%d_%H-%M-%S")
|
||||
.to_string(),
|
||||
.format("%Y-%m-%d_%H-%M-%S"),
|
||||
&suffix
|
||||
),
|
||||
_ => blob.as_file_name().to_string(),
|
||||
@@ -1421,12 +1452,13 @@ fn maybe_encode_words(words: &str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use async_std::fs::File;
|
||||
use async_std::prelude::*;
|
||||
use mailparse::{addrparse_header, MailHeaderMap};
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::chat::{
|
||||
add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
|
||||
self, add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
@@ -1435,9 +1467,7 @@ mod tests {
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
|
||||
use async_std::fs::File;
|
||||
use mailparse::{addrparse_header, MailHeaderMap};
|
||||
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_render_email_address() {
|
||||
let display_name = "ä space";
|
||||
@@ -1688,7 +1718,7 @@ mod tests {
|
||||
let mut new_msg = Message::new(Viewtype::Text);
|
||||
new_msg.set_text(Some("Hi".to_string()));
|
||||
if let Some(q) = quote {
|
||||
new_msg.set_quote(t, q).await?;
|
||||
new_msg.set_quote(t, Some(q)).await?;
|
||||
}
|
||||
let sent = t.send_msg(group_id, &mut new_msg).await;
|
||||
get_subject(t, sent).await
|
||||
@@ -1857,7 +1887,7 @@ mod tests {
|
||||
}
|
||||
|
||||
if reply {
|
||||
new_msg.set_quote(&t, &incoming_msg).await.unwrap();
|
||||
new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap();
|
||||
}
|
||||
|
||||
let mf = MimeFactory::from_msg(&t, &new_msg, false).await.unwrap();
|
||||
@@ -1877,7 +1907,7 @@ mod tests {
|
||||
|
||||
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||
|
||||
let chat_id = chats.get_chat_id(0);
|
||||
let chat_id = chats.get_chat_id(0).unwrap();
|
||||
chat_id.accept(context).await.unwrap();
|
||||
|
||||
let mut new_msg = Message::new(Viewtype::Text);
|
||||
@@ -1917,7 +1947,7 @@ mod tests {
|
||||
|
||||
let rendered_msg = mimefactory.render(context).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&rendered_msg.message).unwrap();
|
||||
let mail = mailparse::parse_mail(rendered_msg.message.as_bytes()).unwrap();
|
||||
assert_eq!(
|
||||
mail.headers
|
||||
.iter()
|
||||
@@ -1927,7 +1957,7 @@ mod tests {
|
||||
"1.0"
|
||||
);
|
||||
|
||||
let _mime_msg = MimeMessage::from_bytes(context, &rendered_msg.message)
|
||||
let _mime_msg = MimeMessage::from_bytes(context, rendered_msg.message.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -2001,8 +2031,9 @@ mod tests {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("this is the text!".to_string()));
|
||||
|
||||
let payload = t.send_msg(chat.id, &mut msg).await.payload();
|
||||
let mut payload = payload.splitn(3, "\r\n\r\n");
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
|
||||
|
||||
let outer = payload.next().unwrap();
|
||||
let inner = payload.next().unwrap();
|
||||
let body = payload.next().unwrap();
|
||||
@@ -2020,8 +2051,8 @@ mod tests {
|
||||
|
||||
// if another message is sent, that one must not contain the avatar
|
||||
// and no artificial multipart/mixed nesting
|
||||
let payload = t.send_msg(chat.id, &mut msg).await.payload();
|
||||
let mut payload = payload.splitn(2, "\r\n\r\n");
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
let mut payload = sent_msg.payload().splitn(2, "\r\n\r\n");
|
||||
let outer = payload.next().unwrap();
|
||||
let body = payload.next().unwrap();
|
||||
|
||||
@@ -2060,13 +2091,36 @@ mod tests {
|
||||
let to = parsed
|
||||
.headers
|
||||
.get_first_header("To")
|
||||
.ok_or_else(|| format_err!("No To: header parsed"))?;
|
||||
.context("no To: header parsed")?;
|
||||
let to = addrparse_header(to)?;
|
||||
let mailbox = to
|
||||
.extract_single_info()
|
||||
.ok_or_else(|| format_err!("To: field does not contain exactly one address"))?;
|
||||
.context("to: field does not contain exactly one address")?;
|
||||
assert_eq!(mailbox.addr, "bob@example.net");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that standard IMF header "From:" comes before non-standard "Autocrypt:" header.
|
||||
#[async_std::test]
|
||||
async fn test_from_before_autocrypt() -> Result<()> {
|
||||
// create chat with bob
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
|
||||
|
||||
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
|
||||
// make sure, `Subject:` stays in the outer header (imf header)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("this is the text!".to_string()));
|
||||
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
let payload = sent_msg.payload();
|
||||
|
||||
assert_eq!(payload.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(payload.match_indices("From:").count(), 1);
|
||||
|
||||
assert!(payload.match_indices("From:").next() < payload.match_indices("Autocrypt:").next());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::io::Cursor;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
@@ -12,8 +13,8 @@ use once_cell::sync::Lazy;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::constants::{Viewtype, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
|
||||
use crate::contact::addr_normalize;
|
||||
use crate::constants::{DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
|
||||
use crate::contact::{addr_normalize, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_get_filemeta, dc_truncate, parse_receive_headers};
|
||||
use crate::dehtml::dehtml;
|
||||
@@ -23,7 +24,7 @@ use crate::format_flowed::unformat_flowed;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::Fingerprint;
|
||||
use crate::location;
|
||||
use crate::message;
|
||||
use crate::message::{self, Viewtype};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::simplify::simplify;
|
||||
@@ -66,6 +67,7 @@ pub struct MimeMessage {
|
||||
pub location_kml: Option<location::Kml>,
|
||||
pub message_kml: Option<location::Kml>,
|
||||
pub(crate) sync_items: Option<SyncItems>,
|
||||
pub(crate) webxdc_status_update: Option<String>,
|
||||
pub(crate) user_avatar: Option<AvatarAction>,
|
||||
pub(crate) group_avatar: Option<AvatarAction>,
|
||||
pub(crate) mdn_reports: Vec<Report>,
|
||||
@@ -135,6 +137,10 @@ pub enum SystemMessage {
|
||||
/// Self-sent-message that contains only json used for multi-device-sync;
|
||||
/// if possible, we attach that to other messages as for locations.
|
||||
MultiDeviceSync = 20,
|
||||
|
||||
// Sync message that contains a json payload
|
||||
// sent to the other webxdc instances
|
||||
WebxdcStatusUpdate = 30,
|
||||
}
|
||||
|
||||
impl Default for SystemMessage {
|
||||
@@ -296,6 +302,7 @@ impl MimeMessage {
|
||||
location_kml: None,
|
||||
message_kml: None,
|
||||
sync_items: None,
|
||||
webxdc_status_update: None,
|
||||
user_avatar: None,
|
||||
group_avatar: None,
|
||||
failure_report: None,
|
||||
@@ -362,6 +369,16 @@ impl MimeMessage {
|
||||
} else if value == "protection-disabled" {
|
||||
self.is_system_message = SystemMessage::ChatProtectionDisabled;
|
||||
}
|
||||
} else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
|
||||
self.is_system_message = SystemMessage::MemberRemovedFromGroup;
|
||||
} else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,16 +414,18 @@ impl MimeMessage {
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn squash_attachment_parts(&mut self) {
|
||||
if let [textpart, filepart] = &self.parts[..] {
|
||||
let need_drop = {
|
||||
textpart.typ == Viewtype::Text
|
||||
&& (filepart.typ == Viewtype::Image
|
||||
|| filepart.typ == Viewtype::Gif
|
||||
|| filepart.typ == Viewtype::Sticker
|
||||
|| filepart.typ == Viewtype::Audio
|
||||
|| filepart.typ == Viewtype::Voice
|
||||
|| filepart.typ == Viewtype::Video
|
||||
|| filepart.typ == Viewtype::File)
|
||||
};
|
||||
let need_drop = textpart.typ == Viewtype::Text
|
||||
&& match filepart.typ {
|
||||
Viewtype::Image
|
||||
| Viewtype::Gif
|
||||
| Viewtype::Sticker
|
||||
| Viewtype::Audio
|
||||
| Viewtype::Voice
|
||||
| Viewtype::Video
|
||||
| Viewtype::File
|
||||
| Viewtype::Webxdc => true,
|
||||
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
|
||||
};
|
||||
|
||||
if need_drop {
|
||||
let mut filepart = self.parts.swap_remove(1);
|
||||
@@ -538,7 +557,7 @@ impl MimeMessage {
|
||||
};
|
||||
|
||||
if let Some(ref subject) = self.get_subject() {
|
||||
if !self.has_chat_version() {
|
||||
if !self.has_chat_version() && self.webxdc_status_update.is_none() {
|
||||
part.msg = subject.to_string();
|
||||
}
|
||||
}
|
||||
@@ -837,6 +856,12 @@ impl MimeMessage {
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Some("status-update") => {
|
||||
if let Some(second) = mail.subparts.get(1) {
|
||||
self.add_single_part_if_known(context, second, is_related)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
if let Some(first) = mail.subparts.get(0) {
|
||||
any_part_added = self
|
||||
@@ -1006,8 +1031,14 @@ impl MimeMessage {
|
||||
if decoded_data.is_empty() {
|
||||
return;
|
||||
}
|
||||
// treat location/message kml file attachments specially
|
||||
if filename.ends_with(".kml") {
|
||||
let reader = Cursor::new(decoded_data);
|
||||
let msg_type = if context
|
||||
.is_webxdc_file(filename, reader)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Viewtype::Webxdc
|
||||
} else if filename.ends_with(".kml") {
|
||||
// XXX what if somebody sends eg an "location-highlights.kml"
|
||||
// attachment unrelated to location streaming?
|
||||
if filename.starts_with("location") || filename.starts_with("message") {
|
||||
@@ -1023,6 +1054,7 @@ impl MimeMessage {
|
||||
}
|
||||
return;
|
||||
}
|
||||
msg_type
|
||||
} else if filename == "multi-device-sync.json" {
|
||||
let serialized = String::from_utf8_lossy(decoded_data)
|
||||
.parse()
|
||||
@@ -1035,7 +1067,15 @@ impl MimeMessage {
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
}
|
||||
} else if filename == "status-update.json" {
|
||||
let serialized = String::from_utf8_lossy(decoded_data)
|
||||
.parse()
|
||||
.unwrap_or_default();
|
||||
self.webxdc_status_update = Some(serialized);
|
||||
return;
|
||||
} else {
|
||||
msg_type
|
||||
};
|
||||
|
||||
/* we have a regular file attachment,
|
||||
write decoded data to new blob object */
|
||||
@@ -1110,7 +1150,7 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_rfc724_mid(&self) -> Option<String> {
|
||||
pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
|
||||
self.get_header(HeaderDef::XMicrosoftOriginalMessageId)
|
||||
.or_else(|| self.get_header(HeaderDef::MessageId))
|
||||
.and_then(|msgid| parse_message_id(msgid).ok())
|
||||
@@ -1175,6 +1215,9 @@ impl MimeMessage {
|
||||
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
|
||||
let original_message_id = report_fields
|
||||
.get_header_value(HeaderDef::OriginalMessageId)
|
||||
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
|
||||
// the original message id into the In-Reply-To header:
|
||||
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
|
||||
.and_then(|v| parse_message_id(&v).ok());
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
@@ -1338,7 +1381,7 @@ impl MimeMessage {
|
||||
pub async fn handle_reports(
|
||||
&self,
|
||||
context: &Context,
|
||||
from_id: u32,
|
||||
from_id: ContactId,
|
||||
sent_timestamp: i64,
|
||||
parts: &[Part],
|
||||
) {
|
||||
@@ -1452,8 +1495,8 @@ async fn update_gossip_peerstates(
|
||||
pub(crate) struct Report {
|
||||
/// Original-Message-ID header
|
||||
///
|
||||
/// It MUST be present if the original message has a Message-ID according to RFC 8098, but MS
|
||||
/// Exchange does not add it nevertheless, in which case it is `None`.
|
||||
/// It MUST be present if the original message has a Message-ID according to RFC 8098.
|
||||
/// In case we can't find it (shouldn't happen), this is None.
|
||||
original_message_id: Option<String>,
|
||||
/// Additional-Message-IDs
|
||||
additional_message_ids: Vec<String>,
|
||||
@@ -3155,10 +3198,32 @@ Message.
|
||||
#[async_std::test]
|
||||
async fn test_ms_exchange_mdn() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let raw =
|
||||
t.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
|
||||
let original =
|
||||
include_bytes!("../test-data/message/ms_exchange_report_original_message.eml");
|
||||
dc_receive_imf(&t, original, "INBOX", false).await?;
|
||||
let original_msg_id = t.get_last_msg().await.id;
|
||||
|
||||
// 1. Test mimeparser directly
|
||||
let mdn =
|
||||
include_bytes!("../test-data/message/ms_exchange_report_disposition_notification.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await?;
|
||||
assert!(!mimeparser.mdn_reports.is_empty());
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn).await?;
|
||||
assert_eq!(mimeparser.mdn_reports.len(), 1);
|
||||
assert_eq!(
|
||||
mimeparser.mdn_reports[0].original_message_id.as_deref(),
|
||||
Some("d5904dc344eeb5deaf9bb44603f0c716@posteo.de")
|
||||
);
|
||||
assert!(mimeparser.mdn_reports[0].additional_message_ids.is_empty());
|
||||
|
||||
// 2. Test that marking the original msg as read works
|
||||
dc_receive_imf(&t, mdn, "INBOX", false).await?;
|
||||
|
||||
assert_eq!(
|
||||
original_msg_id.get_state(&t).await?,
|
||||
MessageState::OutMdnRcvd
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,9 +110,6 @@ pub enum Param {
|
||||
/// For Jobs
|
||||
AlsoMove = b'M',
|
||||
|
||||
/// For Jobs: space-separated list of message recipients
|
||||
Recipients = b'R',
|
||||
|
||||
/// For MDN-sending job
|
||||
MsgId = b'I',
|
||||
|
||||
@@ -171,6 +168,12 @@ pub enum Param {
|
||||
|
||||
/// For Chats: timestamp of protection settings update.
|
||||
ProtectionSettingsTimestamp = b'L',
|
||||
|
||||
/// For Webxdc Message Instances: Current summary
|
||||
WebxdcSummary = b'N',
|
||||
|
||||
/// For Webxdc Message Instances: timestamp of summary update.
|
||||
WebxdcSummaryTimestamp = b'Q',
|
||||
}
|
||||
|
||||
/// An object for handling key=value parameter lists.
|
||||
|
||||
@@ -379,10 +379,9 @@ impl Peerstate {
|
||||
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&SignedPublicKey> {
|
||||
match min_verified {
|
||||
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(),
|
||||
PeerstateVerifiedStatus::Unverified => self
|
||||
.public_key
|
||||
.as_ref()
|
||||
.or_else(|| self.gossip_key.as_ref()),
|
||||
PeerstateVerifiedStatus::Unverified => {
|
||||
self.public_key.as_ref().or(self.gossip_key.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashSet};
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Result};
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use pgp::armor::BlockType;
|
||||
use pgp::composed::{
|
||||
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
|
||||
@@ -346,7 +346,7 @@ pub async fn pk_validate(
|
||||
// OpenPGP signature calculation.
|
||||
let content = content
|
||||
.get(..content.len().saturating_sub(2))
|
||||
.ok_or_else(|| format_err!("index is out of range"))?;
|
||||
.context("index is out of range")?;
|
||||
|
||||
for pkey in pkeys {
|
||||
if standalone_signature.verify(pkey, content).is_ok() {
|
||||
@@ -520,7 +520,6 @@ mod tests {
|
||||
&sig_check_keyring,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| println!("{:?}", err))
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 1);
|
||||
@@ -536,7 +535,6 @@ mod tests {
|
||||
&sig_check_keyring,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| println!("{:?}", err))
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 1);
|
||||
|
||||
@@ -41,7 +41,7 @@ impl PlainText {
|
||||
// as <http://example.org> cannot be handled correctly
|
||||
// (they would become <http://example.org> where the trailing > would become a valid url part).
|
||||
// to avoid double encoding, we escape our html-entities by \r that must not be used in the string elsewhere.
|
||||
let line = line.to_string().replace("\r", "");
|
||||
let line = line.to_string().replace('\r', "");
|
||||
|
||||
let mut line = LINKIFY_MAIL_RE
|
||||
.replace_all(&*line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
|
||||
|
||||
@@ -113,7 +113,7 @@ pub async fn get_provider_info(
|
||||
domain: &str,
|
||||
skip_mx: bool,
|
||||
) -> Option<&'static Provider> {
|
||||
let domain = domain.rsplitn(2, '@').next()?;
|
||||
let domain = domain.rsplit('@').next()?;
|
||||
|
||||
if let Some(provider) = get_provider_by_domain(domain) {
|
||||
return Some(provider);
|
||||
|
||||
@@ -282,57 +282,23 @@ static P_DISROOT: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/disroot",
|
||||
server: vec![],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// dubby.org.md: dubby.org
|
||||
static P_DUBBY_ORG: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
id: "dubby.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/dubby-org",
|
||||
server: vec![
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "dubby.org",
|
||||
hostname: "disroot.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "dubby.org",
|
||||
hostname: "disroot.org",
|
||||
port: 587,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "dubby.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
config_defaults: Some(vec![
|
||||
ConfigDefault {
|
||||
key: Config::BccSelf,
|
||||
value: "1",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::SentboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
},
|
||||
]),
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
oauth2_authorizer: None,
|
||||
@@ -400,7 +366,7 @@ static P_EXAMPLE_COM: Lazy<Provider> = Lazy::new(|| {
|
||||
}
|
||||
});
|
||||
|
||||
// fastmail.md: fastmail.com
|
||||
// fastmail.md: 123mail.org, 150mail.com, 150ml.com, 16mail.com, 2-mail.com, 4email.net, 50mail.com, airpost.net, allmail.net, bestmail.us, cluemail.com, elitemail.org, emailcorner.net, emailengine.net, emailengine.org, emailgroups.net, emailplus.org, emailuser.net, eml.cc, f-m.fm, fast-email.com, fast-mail.org, fastem.com, fastemail.us, fastemailer.com, fastest.cc, fastimap.com, fastmail.cn, fastmail.co.uk, fastmail.com, fastmail.com.au, fastmail.de, fastmail.es, fastmail.fm, fastmail.fr, fastmail.im, fastmail.in, fastmail.jp, fastmail.mx, fastmail.net, fastmail.nl, fastmail.org, fastmail.se, fastmail.to, fastmail.tw, fastmail.uk, fastmail.us, fastmailbox.net, fastmessaging.com, fea.st, fmail.co.uk, fmailbox.com, fmgirl.com, fmguy.com, ftml.net, h-mail.us, hailmail.net, imap-mail.com, imap.cc, imapmail.org, inoutbox.com, internet-e-mail.com, internet-mail.org, internetemails.net, internetmailing.net, jetemail.net, justemail.net, letterboxes.org, mail-central.com, mail-page.com, mailandftp.com, mailas.com, mailbolt.com, mailc.net, mailcan.com, mailforce.net, mailftp.com, mailhaven.com, mailingaddress.org, mailite.com, mailmight.com, mailnew.com, mailsent.net, mailservice.ms, mailup.net, mailworks.org, ml1.net, mm.st, myfastmail.com, mymacmail.com, nospammail.net, ownmail.net, petml.com, postinbox.com, postpro.net, proinbox.com, promessage.com, realemail.net, reallyfast.biz, reallyfast.info, rushpost.com, sent.as, sent.at, sent.com, speedpost.net, speedymail.org, ssl-mail.com, swift-mail.com, the-fastest.net, the-quickest.com, theinternetemail.com, veryfast.biz, veryspeedy.net, warpmail.net, xsmail.com, yepmail.net, your-mail.com
|
||||
static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
id: "fastmail",
|
||||
status: Status::Preparation,
|
||||
@@ -423,13 +389,6 @@ static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "smtp.fastmail.com",
|
||||
port: 587,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
@@ -660,6 +619,35 @@ static P_ICLOUD: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// infomaniak.com.md: ik.me
|
||||
static P_INFOMANIAK_COM: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
id: "infomaniak.com",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/infomaniak-com",
|
||||
server: vec![
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "mail.infomaniak.com",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "mail.infomaniak.com",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: Some(10),
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// kolst.com.md: kolst.com
|
||||
static P_KOLST_COM: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
id: "kolst.com",
|
||||
@@ -688,12 +676,41 @@ static P_KONTENT_COM: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// mail.de.md: mail.de
|
||||
static P_MAIL_DE: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
id: "mail.de",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/mail-de",
|
||||
server: vec![
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "imap.mail.de",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "smtp.mail.de",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// mail.ru.md: mail.ru, inbox.ru, internet.ru, bk.ru, list.ru
|
||||
static P_MAIL_RU: Lazy<Provider> = Lazy::new(|| {
|
||||
Provider {
|
||||
id: "mail.ru",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "Не рекомендуется использовать mail.ru, потому что он разряжает вашу батарею быстрее, чем другие провайдеры.",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru, чтобы mail.ru работал с Delta Chat.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/mail-ru",
|
||||
server: vec![
|
||||
@@ -743,7 +760,22 @@ static P_MAILBOX_ORG: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/mailbox-org",
|
||||
server: vec![],
|
||||
server: vec![
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "imap.mailbox.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "smtp.mailbox.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
@@ -866,7 +898,7 @@ static P_NAVER: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
|
||||
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de
|
||||
static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
id: "outlook.com",
|
||||
status: Status::Ok,
|
||||
@@ -895,7 +927,7 @@ static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
|
||||
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ca, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
|
||||
static P_POSTEO: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
id: "posteo",
|
||||
status: Status::Ok,
|
||||
@@ -924,7 +956,7 @@ static P_POSTEO: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
oauth2_authorizer: None,
|
||||
});
|
||||
|
||||
// protonmail.md: protonmail.com, protonmail.ch
|
||||
// protonmail.md: protonmail.com, protonmail.ch, pm.me
|
||||
static P_PROTONMAIL: Lazy<Provider> = Lazy::new(|| {
|
||||
Provider {
|
||||
id: "protonmail",
|
||||
@@ -967,7 +999,22 @@ static P_RISEUP_NET: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/riseup-net",
|
||||
server: vec![],
|
||||
server: vec![
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "mail.riseup.net",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "mail.riseup.net",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
@@ -1024,7 +1071,22 @@ static P_SYSTEMLI_ORG: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/systemli-org",
|
||||
server: vec![],
|
||||
server: vec![
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "mail.systemli.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "mail.systemli.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
@@ -1421,13 +1483,128 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
|
||||
("comcast.net", &*P_COMCAST),
|
||||
("dismail.de", &*P_DISMAIL_DE),
|
||||
("disroot.org", &*P_DISROOT),
|
||||
("dubby.org", &*P_DUBBY_ORG),
|
||||
("e.email", &*P_E_EMAIL),
|
||||
("espiv.net", &*P_ESPIV_NET),
|
||||
("example.com", &*P_EXAMPLE_COM),
|
||||
("example.org", &*P_EXAMPLE_COM),
|
||||
("example.net", &*P_EXAMPLE_COM),
|
||||
("123mail.org", &*P_FASTMAIL),
|
||||
("150mail.com", &*P_FASTMAIL),
|
||||
("150ml.com", &*P_FASTMAIL),
|
||||
("16mail.com", &*P_FASTMAIL),
|
||||
("2-mail.com", &*P_FASTMAIL),
|
||||
("4email.net", &*P_FASTMAIL),
|
||||
("50mail.com", &*P_FASTMAIL),
|
||||
("airpost.net", &*P_FASTMAIL),
|
||||
("allmail.net", &*P_FASTMAIL),
|
||||
("bestmail.us", &*P_FASTMAIL),
|
||||
("cluemail.com", &*P_FASTMAIL),
|
||||
("elitemail.org", &*P_FASTMAIL),
|
||||
("emailcorner.net", &*P_FASTMAIL),
|
||||
("emailengine.net", &*P_FASTMAIL),
|
||||
("emailengine.org", &*P_FASTMAIL),
|
||||
("emailgroups.net", &*P_FASTMAIL),
|
||||
("emailplus.org", &*P_FASTMAIL),
|
||||
("emailuser.net", &*P_FASTMAIL),
|
||||
("eml.cc", &*P_FASTMAIL),
|
||||
("f-m.fm", &*P_FASTMAIL),
|
||||
("fast-email.com", &*P_FASTMAIL),
|
||||
("fast-mail.org", &*P_FASTMAIL),
|
||||
("fastem.com", &*P_FASTMAIL),
|
||||
("fastemail.us", &*P_FASTMAIL),
|
||||
("fastemailer.com", &*P_FASTMAIL),
|
||||
("fastest.cc", &*P_FASTMAIL),
|
||||
("fastimap.com", &*P_FASTMAIL),
|
||||
("fastmail.cn", &*P_FASTMAIL),
|
||||
("fastmail.co.uk", &*P_FASTMAIL),
|
||||
("fastmail.com", &*P_FASTMAIL),
|
||||
("fastmail.com.au", &*P_FASTMAIL),
|
||||
("fastmail.de", &*P_FASTMAIL),
|
||||
("fastmail.es", &*P_FASTMAIL),
|
||||
("fastmail.fm", &*P_FASTMAIL),
|
||||
("fastmail.fr", &*P_FASTMAIL),
|
||||
("fastmail.im", &*P_FASTMAIL),
|
||||
("fastmail.in", &*P_FASTMAIL),
|
||||
("fastmail.jp", &*P_FASTMAIL),
|
||||
("fastmail.mx", &*P_FASTMAIL),
|
||||
("fastmail.net", &*P_FASTMAIL),
|
||||
("fastmail.nl", &*P_FASTMAIL),
|
||||
("fastmail.org", &*P_FASTMAIL),
|
||||
("fastmail.se", &*P_FASTMAIL),
|
||||
("fastmail.to", &*P_FASTMAIL),
|
||||
("fastmail.tw", &*P_FASTMAIL),
|
||||
("fastmail.uk", &*P_FASTMAIL),
|
||||
("fastmail.us", &*P_FASTMAIL),
|
||||
("fastmailbox.net", &*P_FASTMAIL),
|
||||
("fastmessaging.com", &*P_FASTMAIL),
|
||||
("fea.st", &*P_FASTMAIL),
|
||||
("fmail.co.uk", &*P_FASTMAIL),
|
||||
("fmailbox.com", &*P_FASTMAIL),
|
||||
("fmgirl.com", &*P_FASTMAIL),
|
||||
("fmguy.com", &*P_FASTMAIL),
|
||||
("ftml.net", &*P_FASTMAIL),
|
||||
("h-mail.us", &*P_FASTMAIL),
|
||||
("hailmail.net", &*P_FASTMAIL),
|
||||
("imap-mail.com", &*P_FASTMAIL),
|
||||
("imap.cc", &*P_FASTMAIL),
|
||||
("imapmail.org", &*P_FASTMAIL),
|
||||
("inoutbox.com", &*P_FASTMAIL),
|
||||
("internet-e-mail.com", &*P_FASTMAIL),
|
||||
("internet-mail.org", &*P_FASTMAIL),
|
||||
("internetemails.net", &*P_FASTMAIL),
|
||||
("internetmailing.net", &*P_FASTMAIL),
|
||||
("jetemail.net", &*P_FASTMAIL),
|
||||
("justemail.net", &*P_FASTMAIL),
|
||||
("letterboxes.org", &*P_FASTMAIL),
|
||||
("mail-central.com", &*P_FASTMAIL),
|
||||
("mail-page.com", &*P_FASTMAIL),
|
||||
("mailandftp.com", &*P_FASTMAIL),
|
||||
("mailas.com", &*P_FASTMAIL),
|
||||
("mailbolt.com", &*P_FASTMAIL),
|
||||
("mailc.net", &*P_FASTMAIL),
|
||||
("mailcan.com", &*P_FASTMAIL),
|
||||
("mailforce.net", &*P_FASTMAIL),
|
||||
("mailftp.com", &*P_FASTMAIL),
|
||||
("mailhaven.com", &*P_FASTMAIL),
|
||||
("mailingaddress.org", &*P_FASTMAIL),
|
||||
("mailite.com", &*P_FASTMAIL),
|
||||
("mailmight.com", &*P_FASTMAIL),
|
||||
("mailnew.com", &*P_FASTMAIL),
|
||||
("mailsent.net", &*P_FASTMAIL),
|
||||
("mailservice.ms", &*P_FASTMAIL),
|
||||
("mailup.net", &*P_FASTMAIL),
|
||||
("mailworks.org", &*P_FASTMAIL),
|
||||
("ml1.net", &*P_FASTMAIL),
|
||||
("mm.st", &*P_FASTMAIL),
|
||||
("myfastmail.com", &*P_FASTMAIL),
|
||||
("mymacmail.com", &*P_FASTMAIL),
|
||||
("nospammail.net", &*P_FASTMAIL),
|
||||
("ownmail.net", &*P_FASTMAIL),
|
||||
("petml.com", &*P_FASTMAIL),
|
||||
("postinbox.com", &*P_FASTMAIL),
|
||||
("postpro.net", &*P_FASTMAIL),
|
||||
("proinbox.com", &*P_FASTMAIL),
|
||||
("promessage.com", &*P_FASTMAIL),
|
||||
("realemail.net", &*P_FASTMAIL),
|
||||
("reallyfast.biz", &*P_FASTMAIL),
|
||||
("reallyfast.info", &*P_FASTMAIL),
|
||||
("rushpost.com", &*P_FASTMAIL),
|
||||
("sent.as", &*P_FASTMAIL),
|
||||
("sent.at", &*P_FASTMAIL),
|
||||
("sent.com", &*P_FASTMAIL),
|
||||
("speedpost.net", &*P_FASTMAIL),
|
||||
("speedymail.org", &*P_FASTMAIL),
|
||||
("ssl-mail.com", &*P_FASTMAIL),
|
||||
("swift-mail.com", &*P_FASTMAIL),
|
||||
("the-fastest.net", &*P_FASTMAIL),
|
||||
("the-quickest.com", &*P_FASTMAIL),
|
||||
("theinternetemail.com", &*P_FASTMAIL),
|
||||
("veryfast.biz", &*P_FASTMAIL),
|
||||
("veryspeedy.net", &*P_FASTMAIL),
|
||||
("warpmail.net", &*P_FASTMAIL),
|
||||
("xsmail.com", &*P_FASTMAIL),
|
||||
("yepmail.net", &*P_FASTMAIL),
|
||||
("your-mail.com", &*P_FASTMAIL),
|
||||
("firemail.at", &*P_FIREMAIL_DE),
|
||||
("firemail.de", &*P_FIREMAIL_DE),
|
||||
("five.chat", &*P_FIVE_CHAT),
|
||||
@@ -1451,8 +1628,10 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
|
||||
("icloud.com", &*P_ICLOUD),
|
||||
("me.com", &*P_ICLOUD),
|
||||
("mac.com", &*P_ICLOUD),
|
||||
("ik.me", &*P_INFOMANIAK_COM),
|
||||
("kolst.com", &*P_KOLST_COM),
|
||||
("kontent.com", &*P_KONTENT_COM),
|
||||
("mail.de", &*P_MAIL_DE),
|
||||
("mail.ru", &*P_MAIL_RU),
|
||||
("inbox.ru", &*P_MAIL_RU),
|
||||
("internet.ru", &*P_MAIL_RU),
|
||||
@@ -1469,10 +1648,12 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
|
||||
("office365.com", &*P_OUTLOOK_COM),
|
||||
("outlook.com.tr", &*P_OUTLOOK_COM),
|
||||
("live.com", &*P_OUTLOOK_COM),
|
||||
("outlook.de", &*P_OUTLOOK_COM),
|
||||
("posteo.de", &*P_POSTEO),
|
||||
("posteo.af", &*P_POSTEO),
|
||||
("posteo.at", &*P_POSTEO),
|
||||
("posteo.be", &*P_POSTEO),
|
||||
("posteo.ca", &*P_POSTEO),
|
||||
("posteo.ch", &*P_POSTEO),
|
||||
("posteo.cl", &*P_POSTEO),
|
||||
("posteo.co", &*P_POSTEO),
|
||||
@@ -1521,6 +1702,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
|
||||
("posteo.us", &*P_POSTEO),
|
||||
("protonmail.com", &*P_PROTONMAIL),
|
||||
("protonmail.ch", &*P_PROTONMAIL),
|
||||
("pm.me", &*P_PROTONMAIL),
|
||||
("qq.com", &*P_QQ),
|
||||
("foxmail.com", &*P_QQ),
|
||||
("riseup.net", &*P_RISEUP_NET),
|
||||
@@ -1617,7 +1799,6 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
("comcast", &*P_COMCAST),
|
||||
("dismail.de", &*P_DISMAIL_DE),
|
||||
("disroot", &*P_DISROOT),
|
||||
("dubby.org", &*P_DUBBY_ORG),
|
||||
("e.email", &*P_E_EMAIL),
|
||||
("espiv.net", &*P_ESPIV_NET),
|
||||
("example.com", &*P_EXAMPLE_COM),
|
||||
@@ -1632,8 +1813,10 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
("i.ua", &*P_I_UA),
|
||||
("i3.net", &*P_I3_NET),
|
||||
("icloud", &*P_ICLOUD),
|
||||
("infomaniak.com", &*P_INFOMANIAK_COM),
|
||||
("kolst.com", &*P_KOLST_COM),
|
||||
("kontent.com", &*P_KONTENT_COM),
|
||||
("mail.de", &*P_MAIL_DE),
|
||||
("mail.ru", &*P_MAIL_RU),
|
||||
("mail2tor", &*P_MAIL2TOR),
|
||||
("mailbox.org", &*P_MAILBOX_ORG),
|
||||
@@ -1670,4 +1853,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(2021, 9, 29));
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 1, 31));
|
||||
|
||||
38
src/qr.rs
38
src/qr.rs
@@ -9,7 +9,7 @@ use std::collections::BTreeMap;
|
||||
use crate::chat::{self, get_chat_id_by_grpid, ChatIdBlocked};
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::{addr_normalize, may_be_valid_addr, Contact, Origin};
|
||||
use crate::contact::{addr_normalize, may_be_valid_addr, Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::key::Fingerprint;
|
||||
@@ -30,7 +30,7 @@ const HTTPS_SCHEME: &str = "https://";
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Qr {
|
||||
AskVerifyContact {
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
fingerprint: Fingerprint,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
@@ -38,16 +38,16 @@ pub enum Qr {
|
||||
AskVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
fingerprint: Fingerprint,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
FprOk {
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
},
|
||||
FprMismatch {
|
||||
contact_id: Option<u32>,
|
||||
contact_id: Option<ContactId>,
|
||||
},
|
||||
FprWithoutAddr {
|
||||
fingerprint: String,
|
||||
@@ -60,7 +60,7 @@ pub enum Qr {
|
||||
instance_pattern: String,
|
||||
},
|
||||
Addr {
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
},
|
||||
Url {
|
||||
url: String,
|
||||
@@ -69,7 +69,7 @@ pub enum Qr {
|
||||
text: String,
|
||||
},
|
||||
WithdrawVerifyContact {
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
fingerprint: Fingerprint,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
@@ -77,13 +77,13 @@ pub enum Qr {
|
||||
WithdrawVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
fingerprint: Fingerprint,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
ReviveVerifyContact {
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
fingerprint: Fingerprint,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
@@ -91,7 +91,7 @@ pub enum Qr {
|
||||
ReviveVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
fingerprint: Fingerprint,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
@@ -173,7 +173,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
};
|
||||
|
||||
let name = if let Some(encoded_name) = param.get("n") {
|
||||
let encoded_name = encoded_name.replace("+", "%20"); // sometimes spaces are encoded as `+`
|
||||
let encoded_name = encoded_name.replace('+', "%20"); // sometimes spaces are encoded as `+`
|
||||
match percent_decode_str(&encoded_name).decode_utf8() {
|
||||
Ok(name) => name.to_string(),
|
||||
Err(err) => bail!("Invalid name: {}", err),
|
||||
@@ -188,7 +188,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
|
||||
let grpname = if grpid.is_some() {
|
||||
if let Some(encoded_name) = param.get("g") {
|
||||
let encoded_name = encoded_name.replace("+", "%20"); // sometimes spaces are encoded as `+`
|
||||
let encoded_name = encoded_name.replace('+', "%20"); // sometimes spaces are encoded as `+`
|
||||
match percent_decode_str(&encoded_name).decode_utf8() {
|
||||
Ok(name) => Some(name.to_string()),
|
||||
Err(err) => bail!("Invalid group name: {}", err),
|
||||
@@ -304,14 +304,14 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
fn decode_account(qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.get(DCACCOUNT_SCHEME.len()..)
|
||||
.ok_or_else(|| format_err!("Invalid DCACCOUNT payload"))?;
|
||||
.context("invalid DCACCOUNT payload")?;
|
||||
let url =
|
||||
url::Url::parse(payload).with_context(|| format!("Invalid account URL: {:?}", payload))?;
|
||||
if url.scheme() == "http" || url.scheme() == "https" {
|
||||
Ok(Qr::Account {
|
||||
domain: url
|
||||
.host_str()
|
||||
.ok_or_else(|| format_err!("Can't extract WebRTC instance domain"))?
|
||||
.context("can't extract WebRTC instance domain")?
|
||||
.to_string(),
|
||||
})
|
||||
} else {
|
||||
@@ -323,7 +323,7 @@ fn decode_account(qr: &str) -> Result<Qr> {
|
||||
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.get(DCWEBRTC_SCHEME.len()..)
|
||||
.ok_or_else(|| format_err!("Invalid DCWEBRTC payload"))?;
|
||||
.context("invalid DCWEBRTC payload")?;
|
||||
|
||||
let (_type, url) = Message::parse_webrtc_instance(payload);
|
||||
let url =
|
||||
@@ -333,7 +333,7 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
Ok(Qr::WebrtcInstance {
|
||||
domain: url
|
||||
.host_str()
|
||||
.ok_or_else(|| format_err!("Can't extract WebRTC instance domain"))?
|
||||
.context("can't extract WebRTC instance domain")?
|
||||
.to_string(),
|
||||
instance_pattern: payload.to_string(),
|
||||
})
|
||||
@@ -711,7 +711,7 @@ mod tests {
|
||||
..
|
||||
} = qr
|
||||
{
|
||||
assert_ne!(contact_id, 0);
|
||||
assert_ne!(contact_id, ContactId::new(0));
|
||||
assert_eq!(grpname, "test ? test !");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
@@ -729,7 +729,7 @@ mod tests {
|
||||
..
|
||||
} = qr
|
||||
{
|
||||
assert_ne!(contact_id, 0);
|
||||
assert_ne!(contact_id, ContactId::new(0));
|
||||
assert_eq!(grpname, "test ? test !");
|
||||
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
@@ -751,7 +751,7 @@ mod tests {
|
||||
).await?;
|
||||
|
||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||
assert_ne!(contact_id, 0);
|
||||
assert_ne!(contact_id, ContactId::new(0));
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
//! # Support for IMAP QUOTA extension.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_imap::types::{Quota, QuotaResource};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::imap::scan_folders::get_watched_folders;
|
||||
use crate::imap::Imap;
|
||||
use crate::job::{Action, Status};
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Params;
|
||||
use crate::{job, stock_str, EventType};
|
||||
|
||||
@@ -64,7 +63,7 @@ async fn get_unique_quota_roots_and_usage(
|
||||
.iter()
|
||||
.find(|q| &q.root_name == quota_root_name)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("quota_root should have a quota"))?;
|
||||
.context("quota_root should have a quota")?;
|
||||
// replace old quotas, because between fetching quotaroots for folders,
|
||||
// messages could be recieved and so the usage could have been changed
|
||||
*unique_quota_roots
|
||||
@@ -96,7 +95,7 @@ fn get_highest_usage<'t>(
|
||||
}
|
||||
}
|
||||
|
||||
highest.ok_or_else(|| anyhow!("no quota_resource found, this is unexpected"))
|
||||
highest.context("no quota_resource found, this is unexpected")
|
||||
}
|
||||
|
||||
/// Checks if a quota warning is needed.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{
|
||||
channel::{self, Receiver, Sender},
|
||||
@@ -8,9 +8,11 @@ use async_std::{
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::maybe_add_time_based_warnings;
|
||||
use crate::ephemeral::delete_expired_imap_messages;
|
||||
use crate::imap::Imap;
|
||||
use crate::job::{self, Thread};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::log::LogExt;
|
||||
use crate::smtp::{send_smtp_messages, Smtp};
|
||||
|
||||
use self::connectivity::ConnectivityStore;
|
||||
|
||||
@@ -160,25 +162,57 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
|
||||
return connection.fake_idle(ctx, Some(watch_folder)).await;
|
||||
}
|
||||
|
||||
// Scan other folders before fetching from watched folder. This may result in the
|
||||
// messages being moved into the watched folder, for example from the Spam folder to
|
||||
// the Inbox folder.
|
||||
if folder == Config::ConfiguredInboxFolder {
|
||||
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
|
||||
if let Err(err) = connection.scan_folders(ctx).await {
|
||||
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
|
||||
// but maybe just one folder can't be selected or something
|
||||
warn!(ctx, "{}", err);
|
||||
}
|
||||
// Mark expired messages for deletion.
|
||||
if let Err(err) = delete_expired_imap_messages(ctx)
|
||||
.await
|
||||
.context("delete_expired_imap_messages failed")
|
||||
{
|
||||
warn!(ctx, "{:#}", err);
|
||||
}
|
||||
|
||||
// fetch
|
||||
// Fetch the watched folder.
|
||||
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
warn!(ctx, "{:#}", err);
|
||||
return InterruptInfo::new(false);
|
||||
}
|
||||
|
||||
// Scan additional folders only after finishing fetching the watched folder.
|
||||
//
|
||||
// On iOS the application has strictly limited time to work in background, so we may not
|
||||
// be able to scan all folders before time is up if there are many of them.
|
||||
if folder == Config::ConfiguredInboxFolder {
|
||||
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
|
||||
match connection.scan_folders(ctx).await {
|
||||
Err(err) => {
|
||||
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
|
||||
// but maybe just one folder can't be selected or something
|
||||
warn!(ctx, "{}", err);
|
||||
}
|
||||
Ok(true) => {
|
||||
// Fetch the watched folder again in case scanning other folder moved messages
|
||||
// there.
|
||||
//
|
||||
// In most cases this will select the watched folder and return because there are
|
||||
// no new messages. We want to select the watched folder anyway before going IDLE
|
||||
// there, so this does not take additional protocol round-trip.
|
||||
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
warn!(ctx, "{:#}", err);
|
||||
return InterruptInfo::new(false);
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronize Seen flags.
|
||||
connection
|
||||
.sync_seen_flags(ctx, &watch_folder)
|
||||
.await
|
||||
.context("sync_seen_flags")
|
||||
.ok_or_log(ctx);
|
||||
|
||||
connection.connectivity.set_connected(ctx).await;
|
||||
|
||||
// idle
|
||||
@@ -273,17 +307,25 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
|
||||
|
||||
let mut interrupt_info = Default::default();
|
||||
loop {
|
||||
match job::load_next(&ctx, Thread::Smtp, &interrupt_info)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
let job = match job::load_next(&ctx, Thread::Smtp, &interrupt_info).await {
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed loading job from the database: {:#}.", err);
|
||||
None
|
||||
}
|
||||
Ok(job) => job,
|
||||
};
|
||||
|
||||
match job {
|
||||
Some(job) => {
|
||||
info!(ctx, "executing smtp job");
|
||||
job::perform_job(&ctx, job::Connection::Smtp(&mut connection), job).await;
|
||||
interrupt_info = Default::default();
|
||||
}
|
||||
None => {
|
||||
if let Err(err) = send_smtp_messages(&ctx, &mut connection).await {
|
||||
warn!(ctx, "send_smtp_messages failed: {:#}", err);
|
||||
}
|
||||
|
||||
// Fake Idle
|
||||
info!(ctx, "smtp fake idle - started");
|
||||
match &connection.last_send_error {
|
||||
@@ -333,7 +375,7 @@ impl Scheduler {
|
||||
}))
|
||||
};
|
||||
|
||||
if ctx.get_config_bool(Config::MvboxMove).await? {
|
||||
if ctx.should_watch_mvbox().await? {
|
||||
let ctx = ctx.clone();
|
||||
mvbox_handle = Some(task::spawn(async move {
|
||||
simple_imap_loop(
|
||||
|
||||
@@ -5,6 +5,7 @@ use async_std::sync::{Mutex, RwLockReadGuard};
|
||||
|
||||
use crate::dc_tools::time;
|
||||
use crate::events::EventType;
|
||||
use crate::imap::scan_folders::get_watched_folder_configs;
|
||||
use crate::quota::{
|
||||
QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_MAX_AGE_SECONDS, QUOTA_WARN_THRESHOLD_PERCENTAGE,
|
||||
};
|
||||
@@ -362,17 +363,14 @@ impl Context {
|
||||
[
|
||||
(
|
||||
Config::ConfiguredInboxFolder,
|
||||
None,
|
||||
inbox.state.connectivity.clone(),
|
||||
),
|
||||
(
|
||||
Config::ConfiguredMvboxFolder,
|
||||
Some(Config::MvboxMove),
|
||||
mvbox.state.connectivity.clone(),
|
||||
),
|
||||
(
|
||||
Config::ConfiguredSentboxFolder,
|
||||
Some(Config::SentboxWatch),
|
||||
sentbox.state.connectivity.clone(),
|
||||
),
|
||||
],
|
||||
@@ -391,20 +389,12 @@ impl Context {
|
||||
// - "Sent": Connected
|
||||
// =============================================================================================
|
||||
|
||||
let watched_folders = get_watched_folder_configs(self).await?;
|
||||
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
|
||||
for (folder, watch, state) in &folders_states {
|
||||
let w = if let Some(watch_config) = *watch {
|
||||
self.get_config(watch_config)
|
||||
.await
|
||||
.ok_or_log(self)
|
||||
.flatten()
|
||||
== Some("1".to_string())
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
for (folder, state) in &folders_states {
|
||||
let mut folder_added = false;
|
||||
if w {
|
||||
|
||||
if watched_folders.contains(folder) {
|
||||
let f = self.get_config(*folder).await.ok_or_log(self).flatten();
|
||||
|
||||
if let Some(foldername) = f {
|
||||
|
||||
@@ -3,21 +3,20 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
use async_std::sync::Mutex;
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{self, is_contact_in_chat, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
|
||||
use crate::contact::{Contact, Origin, VerifiedStatus};
|
||||
use crate::constants::{Blocked, DC_CONTACT_ID_LAST_SPECIAL};
|
||||
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::e2ee::ensure_secret_key_exists;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus, ToSave};
|
||||
@@ -25,28 +24,16 @@ use crate::qr::check_qr;
|
||||
use crate::stock_str;
|
||||
use crate::token;
|
||||
|
||||
mod bob;
|
||||
mod bobstate;
|
||||
mod qrinvite;
|
||||
|
||||
use crate::token::Namespace;
|
||||
use bobstate::{BobHandshakeStage, BobState, BobStateHandle};
|
||||
use bobstate::BobState;
|
||||
use qrinvite::QrInvite;
|
||||
|
||||
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
|
||||
|
||||
macro_rules! joiner_progress {
|
||||
($context:tt, $contact_id:expr, $progress:expr) => {
|
||||
assert!(
|
||||
$progress >= 0 && $progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.emit_event($crate::events::EventType::SecurejoinJoinerProgress {
|
||||
contact_id: $contact_id,
|
||||
progress: $progress,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! inviter_progress {
|
||||
($context:tt, $contact_id:expr, $progress:expr) => {
|
||||
assert!(
|
||||
@@ -60,100 +47,6 @@ macro_rules! inviter_progress {
|
||||
};
|
||||
}
|
||||
|
||||
/// State for setup-contact/secure-join protocol joiner's side, aka Bob's side.
|
||||
///
|
||||
/// The setup-contact protocol needs to carry state for both the inviter (Alice) and the
|
||||
/// joiner/invitee (Bob). For Alice this state is minimal and in the `tokens` table in the
|
||||
/// database. For Bob this state is only carried live on the [`Context`] in this struct.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Bob {
|
||||
inner: Mutex<Option<BobState>>,
|
||||
}
|
||||
|
||||
/// Return value for [`Bob::start_protocol`].
|
||||
///
|
||||
/// This indicates which protocol variant was started and provides the required information
|
||||
/// about it.
|
||||
enum StartedProtocolVariant {
|
||||
/// The setup-contact protocol, to verify a contact.
|
||||
SetupContact,
|
||||
/// The secure-join protocol, to join a group.
|
||||
SecureJoin {
|
||||
group_id: String,
|
||||
group_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Bob {
|
||||
/// Starts the securejoin protocol with the QR `invite`.
|
||||
///
|
||||
/// This will try to start the securejoin protocol for the given QR `invite`. If it
|
||||
/// succeeded the protocol state will be tracked in `self`.
|
||||
///
|
||||
/// This function takes care of starting the "ongoing" mechanism if required and
|
||||
/// handling errors while starting the protocol.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// If the started protocol is joining a group the returned struct contains information
|
||||
/// about the group and ongoing process.
|
||||
async fn start_protocol(
|
||||
&self,
|
||||
context: &Context,
|
||||
invite: QrInvite,
|
||||
) -> Result<StartedProtocolVariant, JoinError> {
|
||||
let mut guard = self.inner.lock().await;
|
||||
if guard.is_some() {
|
||||
warn!(context, "The new securejoin will replace the ongoing one.");
|
||||
*guard = None;
|
||||
}
|
||||
let variant = match invite {
|
||||
QrInvite::Group {
|
||||
ref grpid,
|
||||
ref name,
|
||||
..
|
||||
} => StartedProtocolVariant::SecureJoin {
|
||||
group_id: grpid.clone(),
|
||||
group_name: name.clone(),
|
||||
},
|
||||
_ => StartedProtocolVariant::SetupContact,
|
||||
};
|
||||
match BobState::start_protocol(context, invite).await {
|
||||
Ok((state, stage)) => {
|
||||
if matches!(stage, BobHandshakeStage::RequestWithAuthSent) {
|
||||
joiner_progress!(context, state.invite().contact_id(), 400);
|
||||
}
|
||||
*guard = Some(state);
|
||||
Ok(variant)
|
||||
}
|
||||
Err(err) => {
|
||||
if let StartedProtocolVariant::SecureJoin { .. } = variant {
|
||||
context.free_ongoing().await;
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a handle to the [`BobState`] of the handshake.
|
||||
///
|
||||
/// If there currently isn't a handshake running this will return `None`. Otherwise
|
||||
/// this will return a handle to the current [`BobState`]. This handle allows
|
||||
/// processing an incoming message and allows terminating the handshake.
|
||||
///
|
||||
/// The handle contains an exclusive lock, which is held until the handle is dropped.
|
||||
/// This guarantees all state and state changes are correct and allows safely
|
||||
/// terminating the handshake without worrying about concurrency.
|
||||
async fn state(&self, context: &Context) -> Option<BobStateHandle<'_>> {
|
||||
let guard = self.inner.lock().await;
|
||||
let ret = BobStateHandle::from_guard(guard);
|
||||
if ret.is_none() {
|
||||
info!(context, "No active BobState found for securejoin handshake");
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a Secure Join QR code.
|
||||
///
|
||||
/// With `group` set to `None` this generates a setup-contact QR code, with `group` set to a
|
||||
@@ -280,7 +173,7 @@ pub enum JoinError {
|
||||
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
securejoin(context, qr).await.map_err(|err| {
|
||||
warn!(context, "Fatal joiner error: {:#}", err);
|
||||
// This is a modal operation, the user has context on what failed.
|
||||
// The user just scanned this QR code so has context on what failed.
|
||||
error!(context, "QR process failed");
|
||||
err
|
||||
})
|
||||
@@ -297,47 +190,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
|
||||
let invite = QrInvite::try_from(qr_scan)?;
|
||||
|
||||
match context.bob.start_protocol(context, invite.clone()).await? {
|
||||
StartedProtocolVariant::SetupContact => {
|
||||
// for a one-to-one-chat, the chat is already known, return the chat-id,
|
||||
// the verification runs in background
|
||||
let chat_id = ChatId::create_for_contact(context, invite.contact_id())
|
||||
.await
|
||||
.map_err(JoinError::UnknownContact)?;
|
||||
Ok(chat_id)
|
||||
}
|
||||
StartedProtocolVariant::SecureJoin {
|
||||
group_id,
|
||||
group_name,
|
||||
} => {
|
||||
// for a group-join, also create the chat soon and let the verification run in background.
|
||||
// however, the group will become usable only when the protocol has finished.
|
||||
let contact_id = invite.contact_id();
|
||||
let chat_id = if let Some((chat_id, _protected, _blocked)) =
|
||||
chat::get_chat_id_by_grpid(context, &group_id).await?
|
||||
{
|
||||
chat_id.unblock(context).await?;
|
||||
chat_id
|
||||
} else {
|
||||
ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
&group_id,
|
||||
&group_name,
|
||||
Blocked::Not,
|
||||
ProtectionStatus::Unprotected, // protection is added later as needed
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
if !is_contact_in_chat(context, chat_id, contact_id).await? {
|
||||
chat::add_to_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
}
|
||||
let msg = stock_str::secure_join_started(context, contact_id).await;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
Ok(chat_id)
|
||||
}
|
||||
}
|
||||
bob::start_protocol(context, invite).await
|
||||
}
|
||||
|
||||
/// Error when failing to send a protocol handshake message.
|
||||
@@ -352,7 +205,7 @@ pub struct SendMsgError(#[from] anyhow::Error);
|
||||
/// Bob's handshake messages are sent in `BobState::send_handshake_message()`.
|
||||
async fn send_alice_handshake_msg(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
step: &str,
|
||||
fingerprint: Option<Fingerprint>,
|
||||
) -> Result<(), SendMsgError> {
|
||||
@@ -380,7 +233,7 @@ async fn send_alice_handshake_msg(
|
||||
}
|
||||
|
||||
/// Get an unblocked chat that can be used for info messages.
|
||||
async fn info_chat_id(context: &Context, contact_id: u32) -> Result<ChatId> {
|
||||
async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId> {
|
||||
let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?;
|
||||
Ok(chat_id_blocked.id)
|
||||
}
|
||||
@@ -388,7 +241,7 @@ async fn info_chat_id(context: &Context, contact_id: u32) -> Result<ChatId> {
|
||||
async fn fingerprint_equals_sender(
|
||||
context: &Context,
|
||||
fingerprint: &Fingerprint,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
) -> Result<bool, Error> {
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
@@ -442,9 +295,8 @@ pub(crate) enum HandshakeMessage {
|
||||
|
||||
/// Handle incoming secure-join handshake.
|
||||
///
|
||||
/// This function will update the securejoin state in [`InnerContext::bob`] and also
|
||||
/// terminate the ongoing process using [`Context::stop_ongoing`] as required by the
|
||||
/// protocol.
|
||||
/// This function will update the securejoin state in the database as the protocol
|
||||
/// progresses.
|
||||
///
|
||||
/// A message which results in [`Err`] will be hidden from the user but not deleted, it may
|
||||
/// be a valid message for something else we are not aware off. E.g. it could be part of a
|
||||
@@ -452,13 +304,11 @@ pub(crate) enum HandshakeMessage {
|
||||
///
|
||||
/// When `handle_securejoin_handshake()` is called, the message is not yet filed in the
|
||||
/// database; this is done by `receive_imf()` later on as needed.
|
||||
///
|
||||
/// [`InnerContext::bob`]: crate::context::InnerContext::bob
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub(crate) async fn handle_securejoin_handshake(
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
) -> Result<HandshakeMessage> {
|
||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
return Err(Error::msg("Can not be called with special contact ID"));
|
||||
@@ -521,38 +371,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
==== Bob - the joiner's side =====
|
||||
==== Step 4 in "Setup verified contact" protocol =====
|
||||
========================================================*/
|
||||
match context.bob.state(context).await {
|
||||
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
|
||||
Some(BobHandshakeStage::Terminated(why)) => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
bobstate.chat_id(context).await?,
|
||||
why,
|
||||
)
|
||||
.await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
Some(_stage) => {
|
||||
if join_vg {
|
||||
// the message reads "Alice replied, waiting for being added to the group…";
|
||||
// show it only on secure-join and not on setup-contact therefore.
|
||||
let msg = stock_str::secure_join_replies(context, contact_id).await;
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
bobstate.chat_id(context).await?,
|
||||
&msg,
|
||||
time(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
joiner_progress!(context, bobstate.invite().contact_id(), 400);
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
None => Ok(HandshakeMessage::Ignore),
|
||||
},
|
||||
None => Ok(HandshakeMessage::Ignore),
|
||||
}
|
||||
bob::handle_auth_required(context, mime_message).await
|
||||
}
|
||||
"vg-request-with-auth" | "vc-request-with-auth" => {
|
||||
/*==========================================================
|
||||
@@ -683,44 +502,14 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
==== Bob - the joiner's side ====
|
||||
==== Step 7 in "Setup verified contact" protocol ====
|
||||
=======================================================*/
|
||||
info!(context, "matched vc-contact-confirm step");
|
||||
let retval = if join_vg {
|
||||
HandshakeMessage::Propagate
|
||||
} else {
|
||||
HandshakeMessage::Ignore
|
||||
};
|
||||
match context.bob.state(context).await {
|
||||
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
|
||||
Some(BobHandshakeStage::Terminated(why)) => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
bobstate.chat_id(context).await?,
|
||||
why,
|
||||
)
|
||||
.await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
Some(BobHandshakeStage::Completed) => {
|
||||
// Can only be BobHandshakeStage::Completed
|
||||
secure_connection_established(
|
||||
context,
|
||||
contact_id,
|
||||
bobstate.chat_id(context).await?,
|
||||
)
|
||||
.await?;
|
||||
Ok(retval)
|
||||
}
|
||||
Some(_) => {
|
||||
warn!(
|
||||
context,
|
||||
"Impossible state returned from handling handshake message"
|
||||
);
|
||||
Ok(retval)
|
||||
}
|
||||
None => Ok(retval),
|
||||
match BobState::from_db(&context.sql).await? {
|
||||
Some(bobstate) => {
|
||||
bob::handle_contact_confirm(context, bobstate, mime_message).await
|
||||
}
|
||||
None => match join_vg {
|
||||
true => Ok(HandshakeMessage::Propagate),
|
||||
false => Ok(HandshakeMessage::Ignore),
|
||||
},
|
||||
None => Ok(retval),
|
||||
}
|
||||
}
|
||||
"vg-member-added-received" | "vc-contact-confirm-received" => {
|
||||
@@ -782,7 +571,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
pub(crate) async fn observe_securejoin_on_other_device(
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
) -> Result<HandshakeMessage> {
|
||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
return Err(Error::msg("Can not be called with special contact ID"));
|
||||
@@ -847,11 +636,11 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
|
||||
async fn secure_connection_established(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
chat_id: ChatId,
|
||||
) -> Result<(), Error> {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let msg = stock_str::contact_verified(context, contact.get_name_n_addr()).await;
|
||||
let msg = stock_str::contact_verified(context, &contact).await;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
Ok(())
|
||||
@@ -859,20 +648,12 @@ async fn secure_connection_established(
|
||||
|
||||
async fn could_not_establish_secure_connection(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
chat_id: ChatId,
|
||||
details: &str,
|
||||
) -> Result<(), Error> {
|
||||
let contact = Contact::get_by_id(context, contact_id).await;
|
||||
let msg = stock_str::contact_not_verified(
|
||||
context,
|
||||
if let Ok(ref contact) = contact {
|
||||
contact.get_addr()
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let msg = stock_str::contact_not_verified(context, &contact).await;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
warn!(
|
||||
context,
|
||||
@@ -942,30 +723,34 @@ mod tests {
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::test_utils::{LogSink, TestContext};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact() -> Result<()> {
|
||||
let (log_tx, _log_sink) = LogSink::create();
|
||||
let alice = TestContext::builder()
|
||||
.configure_alice()
|
||||
.with_log_sink(log_tx.clone())
|
||||
.build()
|
||||
.await;
|
||||
let bob = TestContext::builder()
|
||||
.configure_bob()
|
||||
.with_log_sink(log_tx)
|
||||
.build()
|
||||
.await;
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
async fn test_setup_contact() {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
0
|
||||
);
|
||||
|
||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = dc_get_securejoin_qr(&alice.ctx, None).await?;
|
||||
let qr = dc_get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||
|
||||
// Step 2: Bob scans QR-code, sends vc-request
|
||||
dc_join_securejoin(&bob.ctx, &qr).await?;
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!bob.ctx.has_ongoing().await);
|
||||
@@ -977,7 +762,13 @@ mod tests {
|
||||
|
||||
// Step 3: Alice receives vc-request, sends vc-auth-required
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
@@ -1039,21 +830,30 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&alice.ctx).await?,
|
||||
contact_bob.is_verified(&alice.ctx).await.unwrap(),
|
||||
VerifiedStatus::Unverified
|
||||
);
|
||||
|
||||
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&alice.ctx).await?,
|
||||
contact_bob.is_verified(&alice.ctx).await.unwrap(),
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
|
||||
// exactly one one-to-one chat should be visible for both now
|
||||
// (check this before calling alice.create_chat() explicitly below)
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
|
||||
// Check Alice got the verified message in her 1:1 chat.
|
||||
{
|
||||
@@ -1093,14 +893,14 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&bob.ctx).await?,
|
||||
contact_bob.is_verified(&bob.ctx).await.unwrap(),
|
||||
VerifiedStatus::Unverified
|
||||
);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
contact_alice.is_verified(&bob.ctx).await?,
|
||||
contact_alice.is_verified(&bob.ctx).await.unwrap(),
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
|
||||
@@ -1131,7 +931,6 @@ mod tests {
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-contact-confirm-received"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -1143,17 +942,9 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
let (log_tx, _log_sink) = LogSink::create();
|
||||
let alice = TestContext::builder()
|
||||
.configure_alice()
|
||||
.with_log_sink(log_tx.clone())
|
||||
.build()
|
||||
.await;
|
||||
let bob = TestContext::builder()
|
||||
.configure_bob()
|
||||
.with_log_sink(log_tx)
|
||||
.build()
|
||||
.await;
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Ensure Bob knows Alice_FP
|
||||
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await?;
|
||||
@@ -1276,17 +1067,9 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact_concurrent_calls() -> Result<()> {
|
||||
let (log_tx, _log_sink) = LogSink::create();
|
||||
let alice = TestContext::builder()
|
||||
.configure_alice()
|
||||
.with_log_sink(log_tx.clone())
|
||||
.build()
|
||||
.await;
|
||||
let bob = TestContext::builder()
|
||||
.configure_bob()
|
||||
.with_log_sink(log_tx)
|
||||
.build()
|
||||
.await;
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// do a scan that is not working as claire is never responding
|
||||
let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012";
|
||||
@@ -1315,25 +1098,19 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_secure_join() -> Result<()> {
|
||||
let (log_tx, _log_sink) = LogSink::create();
|
||||
let alice = TestContext::builder()
|
||||
.configure_alice()
|
||||
.with_log_sink(log_tx.clone())
|
||||
.build()
|
||||
.await;
|
||||
let bob = TestContext::builder()
|
||||
.configure_bob()
|
||||
.with_log_sink(log_tx)
|
||||
.build()
|
||||
.await;
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// We start with empty chatlists.
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
|
||||
let chatid =
|
||||
let alice_chatid =
|
||||
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
|
||||
|
||||
// Step 1: Generate QR-code, secure-join implied by chatid
|
||||
let qr = dc_get_securejoin_qr(&alice.ctx, Some(chatid))
|
||||
let qr = dc_get_securejoin_qr(&alice.ctx, Some(alice_chatid))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1424,6 +1201,35 @@ mod tests {
|
||||
"vg-member-added"
|
||||
);
|
||||
|
||||
{
|
||||
// Now Alice's chat with Bob should still be hidden, the verified message should
|
||||
// appear in the group chat.
|
||||
|
||||
let chat = alice
|
||||
.get_chat(&bob)
|
||||
.await
|
||||
.expect("Alice has no 1:1 chat with bob");
|
||||
assert_eq!(
|
||||
chat.blocked,
|
||||
Blocked::Yes,
|
||||
"Alice's 1:1 chat with Bob is not hidden"
|
||||
);
|
||||
let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid, 0x1, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
chat::ChatItem::Message { msg_id } => Some(msg_id),
|
||||
_ => None,
|
||||
})
|
||||
.min()
|
||||
.expect("No messages in Alice's group chat");
|
||||
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
|
||||
assert!(msg.is_info());
|
||||
let text = msg.get_text().unwrap();
|
||||
assert!(text.contains("bob@example.net verified"));
|
||||
}
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
@@ -1438,10 +1244,53 @@ mod tests {
|
||||
|
||||
// Step 7: Bob receives vg-member-added, sends vg-member-added-received
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
contact_alice.is_verified(&bob.ctx).await?,
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
{
|
||||
// Bob has Alice verified, message shows up in the group chat.
|
||||
assert_eq!(
|
||||
contact_alice.is_verified(&bob.ctx).await?,
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
let chat = bob
|
||||
.get_chat(&alice)
|
||||
.await
|
||||
.expect("Bob has no 1:1 chat with Alice");
|
||||
assert_eq!(
|
||||
chat.blocked,
|
||||
Blocked::Yes,
|
||||
"Bob's 1:1 chat with Alice is not hidden"
|
||||
);
|
||||
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid, 0x1, None)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
if let chat::ChatItem::Message { msg_id } = item {
|
||||
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
|
||||
let text = msg.get_text().unwrap();
|
||||
println!("msg {} text: {}", msg_id, text);
|
||||
}
|
||||
}
|
||||
let mut msg_iter = chat::get_chat_msgs(&bob.ctx, bob_chatid, 0x1, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter();
|
||||
loop {
|
||||
match msg_iter.next() {
|
||||
Some(chat::ChatItem::Message { msg_id }) => {
|
||||
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
|
||||
let text = msg.get_text().unwrap();
|
||||
match text.contains("alice@example.org verified") {
|
||||
true => {
|
||||
assert!(msg.is_info());
|
||||
break;
|
||||
}
|
||||
false => continue,
|
||||
}
|
||||
}
|
||||
Some(_) => continue,
|
||||
None => panic!("Verified message not found in Bob's group chat"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
|
||||
257
src/securejoin/bob.rs
Normal file
257
src/securejoin/bob.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
//! Bob's side of SecureJoin handling.
|
||||
//!
|
||||
//! This are some helper functions around [`BobState`] which augment the state changes with
|
||||
//! the required user interactions.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::chat::{is_contact_in_chat, ChatId, ProtectionStatus};
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::events::EventType;
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::{chat, stock_str};
|
||||
|
||||
use super::bobstate::{BobHandshakeStage, BobState};
|
||||
use super::qrinvite::QrInvite;
|
||||
use super::{HandshakeMessage, JoinError};
|
||||
|
||||
/// Starts the securejoin protocol with the QR `invite`.
|
||||
///
|
||||
/// This will try to start the securejoin protocol for the given QR `invite`. If it
|
||||
/// succeeded the protocol state will be tracked in `self`.
|
||||
///
|
||||
/// This function takes care of handling multiple concurrent joins and handling errors while
|
||||
/// starting the protocol.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1
|
||||
/// chat with Alice, for a SecureJoin QR this is the group chat.
|
||||
pub(super) async fn start_protocol(
|
||||
context: &Context,
|
||||
invite: QrInvite,
|
||||
) -> Result<ChatId, JoinError> {
|
||||
// A 1:1 chat is needed to send messages to Alice. When joining a group this chat is
|
||||
// hidden, if a user starts sending messages in it it will be unhidden in
|
||||
// dc_receive_imf.
|
||||
let hidden = match invite {
|
||||
QrInvite::Contact { .. } => Blocked::Not,
|
||||
QrInvite::Group { .. } => Blocked::Yes,
|
||||
};
|
||||
let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
|
||||
.await
|
||||
.map_err(JoinError::UnknownContact)?;
|
||||
|
||||
// Now start the protocol and initialise the state
|
||||
let (state, stage, aborted_states) =
|
||||
BobState::start_protocol(context, invite.clone(), chat_id).await?;
|
||||
for state in aborted_states {
|
||||
error!(context, "Aborting previously unfinished QR Join process.");
|
||||
state.notify_aborted(context, "new QR scanned").await?;
|
||||
state.emit_progress(context, JoinerProgress::Error);
|
||||
}
|
||||
if matches!(stage, BobHandshakeStage::RequestWithAuthSent) {
|
||||
state.emit_progress(context, JoinerProgress::RequestWithAuthSent);
|
||||
}
|
||||
match invite {
|
||||
QrInvite::Group { .. } => {
|
||||
// For a secure-join we need to create the group and add the contact. The group will
|
||||
// only become usable once the protocol is finished.
|
||||
// TODO: how does this group become usable?
|
||||
let group_chat_id = state.joining_chat_id(context).await?;
|
||||
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
|
||||
chat::add_to_chat_contacts_table(context, group_chat_id, invite.contact_id())
|
||||
.await?;
|
||||
}
|
||||
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
|
||||
chat::add_info_msg(context, group_chat_id, &msg, time()).await?;
|
||||
Ok(group_chat_id)
|
||||
}
|
||||
QrInvite::Contact { .. } => {
|
||||
// For setup-contact the BobState already ensured the 1:1 chat exists because it
|
||||
// uses it to send the handshake messages.
|
||||
Ok(state.alice_chat())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles `vc-auth-required` and `vg-auth-required` handshake messages.
|
||||
///
|
||||
/// # Bob - the joiner's side
|
||||
/// ## Step 4 in the "Setup Contact protocol"
|
||||
pub(super) async fn handle_auth_required(
|
||||
context: &Context,
|
||||
message: &MimeMessage,
|
||||
) -> Result<HandshakeMessage> {
|
||||
match BobState::from_db(&context.sql).await? {
|
||||
Some(mut bobstate) => match bobstate.handle_message(context, message).await? {
|
||||
Some(BobHandshakeStage::Terminated(why)) => {
|
||||
bobstate.notify_aborted(context, why).await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
Some(_stage) => {
|
||||
if bobstate.is_join_group() {
|
||||
// The message reads "Alice replied, waiting to be added to the group…",
|
||||
// so only show it on secure-join and not on setup-contact.
|
||||
let contact_id = bobstate.invite().contact_id();
|
||||
let msg = stock_str::secure_join_replies(context, contact_id).await;
|
||||
let chat_id = bobstate.joining_chat_id(context).await?;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
}
|
||||
bobstate.emit_progress(context, JoinerProgress::RequestWithAuthSent);
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
None => Ok(HandshakeMessage::Ignore),
|
||||
},
|
||||
None => Ok(HandshakeMessage::Ignore),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles `vc-contact-confirm` and `vg-member-added` handshake messages.
|
||||
///
|
||||
/// # Bob - the joiner's side
|
||||
/// ## Step 4 in the "Setup Contact protocol"
|
||||
pub(super) async fn handle_contact_confirm(
|
||||
context: &Context,
|
||||
mut bobstate: BobState,
|
||||
message: &MimeMessage,
|
||||
) -> Result<HandshakeMessage> {
|
||||
let retval = if bobstate.is_join_group() {
|
||||
HandshakeMessage::Propagate
|
||||
} else {
|
||||
HandshakeMessage::Ignore
|
||||
};
|
||||
match bobstate.handle_message(context, message).await? {
|
||||
Some(BobHandshakeStage::Terminated(why)) => {
|
||||
bobstate.notify_aborted(context, why).await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
Some(BobHandshakeStage::Completed) => {
|
||||
// Note this goes to the 1:1 chat, as when joining a group we implicitly also
|
||||
// verify both contacts (this could be a bug/security issue, see
|
||||
// e.g. https://github.com/deltachat/deltachat-core-rust/issues/1177).
|
||||
bobstate.notify_peer_verified(context).await?;
|
||||
Ok(retval)
|
||||
}
|
||||
Some(_) => {
|
||||
warn!(
|
||||
context,
|
||||
"Impossible state returned from handling handshake message"
|
||||
);
|
||||
Ok(retval)
|
||||
}
|
||||
None => Ok(retval),
|
||||
}
|
||||
}
|
||||
|
||||
/// Private implementations for user interactions about this [`BobState`].
|
||||
impl BobState {
|
||||
fn is_join_group(&self) -> bool {
|
||||
match self.invite() {
|
||||
QrInvite::Contact { .. } => false,
|
||||
QrInvite::Group { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_progress(&self, context: &Context, progress: JoinerProgress) {
|
||||
let contact_id = self.invite().contact_id();
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress: progress.into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the [`ChatId`] of the chat being joined.
|
||||
///
|
||||
/// This is the chat in which you want to notify the user as well.
|
||||
///
|
||||
/// When joining a group this is the [`ChatId`] of the group chat, when verifying a
|
||||
/// contact this is the [`ChatId`] of the 1:1 chat. The 1:1 chat is assumed to exist
|
||||
/// because a [`BobState`] can not exist without, the group chat will be created if it
|
||||
/// does not yet exist.
|
||||
async fn joining_chat_id(&self, context: &Context) -> Result<ChatId> {
|
||||
match self.invite() {
|
||||
QrInvite::Contact { .. } => Ok(self.alice_chat()),
|
||||
QrInvite::Group {
|
||||
ref grpid,
|
||||
ref name,
|
||||
..
|
||||
} => {
|
||||
let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
|
||||
Some((chat_id, _protected, _blocked)) => {
|
||||
chat_id.unblock(context).await?;
|
||||
chat_id
|
||||
}
|
||||
None => {
|
||||
ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
grpid,
|
||||
name,
|
||||
Blocked::Not,
|
||||
ProtectionStatus::Unprotected, // protection is added later as needed
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
Ok(group_chat_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies the user that the SecureJoin was aborted.
|
||||
///
|
||||
/// This creates an info message in the chat being joined.
|
||||
async fn notify_aborted(&self, context: &Context, why: &str) -> Result<()> {
|
||||
let contact = Contact::get_by_id(context, self.invite().contact_id()).await?;
|
||||
let msg = stock_str::contact_not_verified(context, &contact).await;
|
||||
let chat_id = self.joining_chat_id(context).await?;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
warn!(
|
||||
context,
|
||||
"StockMessage::ContactNotVerified posted to joining chat ({})", why
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Notifies the user that the SecureJoin peer is verified.
|
||||
///
|
||||
/// This creates an info message in the chat being joined.
|
||||
async fn notify_peer_verified(&self, context: &Context) -> Result<()> {
|
||||
let contact = Contact::get_by_id(context, self.invite().contact_id()).await?;
|
||||
let msg = stock_str::contact_verified(context, &contact).await;
|
||||
let chat_id = self.joining_chat_id(context).await?;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Progress updates for [`EventType::SecurejoinJoinerProgress`].
|
||||
///
|
||||
/// This has an `From<JoinerProgress> for usize` impl yielding numbers between 0 and a 1000
|
||||
/// which can be shown as a progress bar.
|
||||
enum JoinerProgress {
|
||||
/// An error occurred.
|
||||
Error,
|
||||
/// vg-vc-request-with-auth sent.
|
||||
///
|
||||
/// Typically shows as "alice@addr verified, introducing myself."
|
||||
RequestWithAuthSent,
|
||||
// /// Completed securejoin.
|
||||
// Succeeded,
|
||||
}
|
||||
|
||||
impl From<JoinerProgress> for usize {
|
||||
fn from(progress: JoinerProgress) -> Self {
|
||||
match progress {
|
||||
JoinerProgress::Error => 0,
|
||||
JoinerProgress::RequestWithAuthSent => 400,
|
||||
// JoinerProgress::Succeeded => 1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,22 +5,21 @@
|
||||
//! provides all the information to its driver so it can perform the correct interactions.
|
||||
//!
|
||||
//! The [`BobState`] is only directly used to initially create it when starting the
|
||||
//! protocol. Afterwards it must be stored in a mutex and the [`BobStateHandle`] should be
|
||||
//! used to work with the state.
|
||||
//! protocol.
|
||||
|
||||
use anyhow::{bail, Error, Result};
|
||||
use async_std::sync::MutexGuard;
|
||||
use anyhow::{Error, Result};
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::constants::{Blocked, Viewtype};
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::sql::Sql;
|
||||
|
||||
use super::qrinvite::QrInvite;
|
||||
use super::{
|
||||
@@ -46,123 +45,14 @@ pub enum BobHandshakeStage {
|
||||
Terminated(&'static str),
|
||||
}
|
||||
|
||||
/// A handle to work with the [`BobState`] of Bob's securejoin protocol.
|
||||
/// The securejoin state kept while Bob is joining.
|
||||
///
|
||||
/// This handle can only be created for when an underlying [`BobState`] exists. It keeps
|
||||
/// open a lock which guarantees unique access to the state and this struct must be dropped
|
||||
/// to return the lock.
|
||||
pub struct BobStateHandle<'a> {
|
||||
guard: MutexGuard<'a, Option<BobState>>,
|
||||
bobstate: BobState,
|
||||
clear_state_on_drop: bool,
|
||||
}
|
||||
|
||||
impl<'a> BobStateHandle<'a> {
|
||||
/// Creates a new instance, upholding the guarantee that [`BobState`] must exist.
|
||||
pub fn from_guard(mut guard: MutexGuard<'a, Option<BobState>>) -> Option<Self> {
|
||||
guard.take().map(|bobstate| Self {
|
||||
guard,
|
||||
bobstate,
|
||||
clear_state_on_drop: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the [`ChatId`] of the group chat to join or the 1:1 chat with Alice.
|
||||
pub async fn chat_id(&self, context: &Context) -> Result<ChatId> {
|
||||
match self.bobstate.invite {
|
||||
QrInvite::Group { ref grpid, .. } => {
|
||||
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, grpid).await? {
|
||||
Ok(chat_id)
|
||||
} else {
|
||||
bail!("chat not found")
|
||||
}
|
||||
}
|
||||
QrInvite::Contact { .. } => Ok(self.bobstate.chat_id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`QrInvite`] of the joiner process.
|
||||
pub fn invite(&self) -> &QrInvite {
|
||||
&self.bobstate.invite
|
||||
}
|
||||
|
||||
/// Handles the given message for the securejoin handshake for Bob.
|
||||
///
|
||||
/// This proxies to [`BobState::handle_message`] and makes sure to clear the state when
|
||||
/// the protocol state is terminal. It returns `Some` if the message successfully
|
||||
/// advanced the state of the protocol state machine, `None` otherwise.
|
||||
pub async fn handle_message(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
) -> Option<BobHandshakeStage> {
|
||||
info!(context, "Handling securejoin message for BobStateHandle");
|
||||
match self.bobstate.handle_message(context, mime_message).await {
|
||||
Ok(Some(stage)) => {
|
||||
if matches!(
|
||||
stage,
|
||||
BobHandshakeStage::Completed | BobHandshakeStage::Terminated(_)
|
||||
) {
|
||||
self.finish_protocol(context).await;
|
||||
}
|
||||
Some(stage)
|
||||
}
|
||||
Ok(None) => None,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Error handling handshake message, aborting handshake: {}", err
|
||||
);
|
||||
self.finish_protocol(context).await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the bob handshake as finished.
|
||||
///
|
||||
/// This will clear the state on [`InnerContext::bob`] once this handle is dropped,
|
||||
/// allowing a new handshake to be started from [`Bob`].
|
||||
///
|
||||
/// Note that the state is only cleared on Drop since otherwise the invariant that the
|
||||
/// state is always consistent is violated. However the "ongoing" process is released
|
||||
/// here a little bit earlier as this requires access to the Context, which we do not
|
||||
/// have on Drop (Drop can not run asynchronous code). Stopping the "ongoing" process
|
||||
/// will release [`securejoin`](super::securejoin) which in turn will finally free the
|
||||
/// ongoing process using [`Context::free_ongoing`].
|
||||
///
|
||||
/// [`InnerContext::bob`]: crate::context::InnerContext::bob
|
||||
/// [`Bob`]: super::Bob
|
||||
async fn finish_protocol(&mut self, context: &Context) {
|
||||
info!(context, "Finishing securejoin handshake protocol for Bob");
|
||||
self.clear_state_on_drop = true;
|
||||
if let QrInvite::Group { .. } = self.bobstate.invite {
|
||||
context.stop_ongoing().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for BobStateHandle<'a> {
|
||||
fn drop(&mut self) {
|
||||
if self.clear_state_on_drop {
|
||||
// The Option should already be empty because we take it out in the ctor,
|
||||
// however the typesystem doesn't guarantee this so do it again anyway.
|
||||
self.guard.take();
|
||||
} else {
|
||||
// Make sure to put back the BobState into the Option of the Mutex, it was taken
|
||||
// out by the constructor.
|
||||
self.guard.replace(self.bobstate.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The securejoin state kept in-memory while Bob is joining.
|
||||
/// This is stored in the database and loaded from there using [`BobState::from_db`]. To
|
||||
/// create a new one use [`BobState::start_protocol`].
|
||||
///
|
||||
/// This is currently stored in [`Bob`] which is stored on the [`Context`], thus Bob can
|
||||
/// only run one securejoin joiner protocol at a time.
|
||||
///
|
||||
/// This purposefully has nothing optional, the state is always fully valid. See
|
||||
/// [`Bob::state`] to get access to this state.
|
||||
/// This purposefully has nothing optional, the state is always fully valid. However once a
|
||||
/// terminal state is reached in [`BobState::next`] the entry in the database will already
|
||||
/// have been deleted.
|
||||
///
|
||||
/// # Conducting the securejoin handshake
|
||||
///
|
||||
@@ -177,6 +67,8 @@ impl<'a> Drop for BobStateHandle<'a> {
|
||||
/// [`Bob::state`]: super::Bob::state
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BobState {
|
||||
/// Database primary key.
|
||||
id: i64,
|
||||
/// The QR Invite code.
|
||||
invite: QrInvite,
|
||||
/// The next expected message from Alice.
|
||||
@@ -188,39 +80,120 @@ pub struct BobState {
|
||||
impl BobState {
|
||||
/// Starts the securejoin protocol and creates a new [`BobState`].
|
||||
///
|
||||
/// The `chat_id` needs to be the ID of the 1:1 chat with Alice, this chat will be used
|
||||
/// to exchange the SecureJoin handshake messages as well as for showing error messages.
|
||||
///
|
||||
/// # Bob - the joiner's side
|
||||
/// ## Step 2 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0
|
||||
///
|
||||
/// This currently aborts any other securejoin process if any did not yet complete. The
|
||||
/// ChatIds of the relevant 1:1 chat of any aborted handshakes are returned so that you
|
||||
/// can report the aboreted handshake in the chat. (Yes, there can only ever be one
|
||||
/// ChatId in that Vec, the database doesn't care though.)
|
||||
pub async fn start_protocol(
|
||||
context: &Context,
|
||||
invite: QrInvite,
|
||||
) -> Result<(Self, BobHandshakeStage), JoinError> {
|
||||
let chat_id =
|
||||
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), Blocked::Yes)
|
||||
.await
|
||||
.map_err(JoinError::UnknownContact)?;
|
||||
if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await? {
|
||||
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
|
||||
info!(context, "Taking securejoin protocol shortcut");
|
||||
let state = Self {
|
||||
invite,
|
||||
next: SecureJoinStep::ContactConfirm,
|
||||
chat_id,
|
||||
chat_id: ChatId,
|
||||
) -> Result<(Self, BobHandshakeStage, Vec<Self>), JoinError> {
|
||||
let (stage, next) =
|
||||
if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await?
|
||||
{
|
||||
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
|
||||
info!(context, "Taking securejoin protocol shortcut");
|
||||
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth)
|
||||
.await?;
|
||||
(
|
||||
BobHandshakeStage::RequestWithAuthSent,
|
||||
SecureJoinStep::ContactConfirm,
|
||||
)
|
||||
} else {
|
||||
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?;
|
||||
(BobHandshakeStage::RequestSent, SecureJoinStep::AuthRequired)
|
||||
};
|
||||
state
|
||||
.send_handshake_message(context, BobHandshakeMsg::RequestWithAuth)
|
||||
.await?;
|
||||
Ok((state, BobHandshakeStage::RequestWithAuthSent))
|
||||
} else {
|
||||
let state = Self {
|
||||
invite,
|
||||
next: SecureJoinStep::AuthRequired,
|
||||
chat_id,
|
||||
};
|
||||
state
|
||||
.send_handshake_message(context, BobHandshakeMsg::Request)
|
||||
.await?;
|
||||
Ok((state, BobHandshakeStage::RequestSent))
|
||||
}
|
||||
let (id, aborted_states) =
|
||||
Self::insert_new_db_entry(context, next, invite.clone(), chat_id).await?;
|
||||
let state = Self {
|
||||
id,
|
||||
invite,
|
||||
next,
|
||||
chat_id,
|
||||
};
|
||||
Ok((state, stage, aborted_states))
|
||||
}
|
||||
|
||||
/// Inserts a new entry in the bobstate table, deleting all previous entries.
|
||||
///
|
||||
/// Returns the ID of the newly inserted entry and all the aborted states.
|
||||
async fn insert_new_db_entry(
|
||||
context: &Context,
|
||||
next: SecureJoinStep,
|
||||
invite: QrInvite,
|
||||
chat_id: ChatId,
|
||||
) -> Result<(i64, Vec<Self>)> {
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
// We need to start a write transaction right away, so that we have the
|
||||
// database locked and no one else can write to this table while we read the
|
||||
// rows that we will delete. So start with a dummy UPDATE.
|
||||
transaction.execute(
|
||||
r#"UPDATE bobstate SET next_step=?;"#,
|
||||
params![SecureJoinStep::Terminated],
|
||||
)?;
|
||||
let mut stmt = transaction.prepare("SELECT id FROM bobstate;")?;
|
||||
let mut aborted = Vec::new();
|
||||
for id in stmt.query_map(params![], |row| row.get::<_, i64>(0))? {
|
||||
let id = id?;
|
||||
let state = BobState::from_db_id(transaction, id)?;
|
||||
aborted.push(state);
|
||||
}
|
||||
|
||||
// Finally delete everything and insert new row.
|
||||
transaction.execute("DELETE FROM bobstate;", params![])?;
|
||||
transaction.execute(
|
||||
"INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
|
||||
params![invite, next, chat_id],
|
||||
)?;
|
||||
let id = transaction.last_insert_rowid();
|
||||
Ok((id, aborted))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Load [`BobState`] from the database.
|
||||
pub async fn from_db(sql: &Sql) -> Result<Option<Self>> {
|
||||
// Because of how Self::start_protocol() updates the database we are currently
|
||||
// guaranteed to only have one row.
|
||||
sql.query_row_optional(
|
||||
"SELECT id, invite, next_step, chat_id FROM bobstate;",
|
||||
paramsv![],
|
||||
|row| {
|
||||
let s = BobState {
|
||||
id: row.get(0)?,
|
||||
invite: row.get(1)?,
|
||||
next: row.get(2)?,
|
||||
chat_id: row.get(3)?,
|
||||
};
|
||||
Ok(s)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn from_db_id(connection: &Connection, id: i64) -> rusqlite::Result<Self> {
|
||||
connection.query_row(
|
||||
"SELECT invite, next_step, chat_id FROM bobstate WHERE id=?;",
|
||||
params![id],
|
||||
|row| {
|
||||
let s = BobState {
|
||||
id,
|
||||
invite: row.get(0)?,
|
||||
next: row.get(1)?,
|
||||
chat_id: row.get(2)?,
|
||||
};
|
||||
Ok(s)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the [`QrInvite`] used to create this [`BobState`].
|
||||
@@ -228,20 +201,45 @@ impl BobState {
|
||||
&self.invite
|
||||
}
|
||||
|
||||
/// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice).
|
||||
pub fn alice_chat(&self) -> ChatId {
|
||||
self.chat_id
|
||||
}
|
||||
|
||||
/// Updates the [`BobState::next`] field in memory and the database.
|
||||
///
|
||||
/// If the next state is a terminal state it will remove this [`BobState`] from the
|
||||
/// database.
|
||||
///
|
||||
/// If a user scanned a new QR code after this [`BobState`] was loaded this update will
|
||||
/// fail currently because starting a new joiner process currently kills any previously
|
||||
/// running processes. This is a limitation which will go away in the future.
|
||||
async fn update_next(&mut self, sql: &Sql, next: SecureJoinStep) -> Result<()> {
|
||||
// TODO: write test verifying how this would fail.
|
||||
match next {
|
||||
SecureJoinStep::AuthRequired | SecureJoinStep::ContactConfirm => {
|
||||
sql.execute(
|
||||
"UPDATE bobstate SET next_step=? WHERE id=?;",
|
||||
paramsv![next, self.id],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
SecureJoinStep::Terminated | SecureJoinStep::Completed => {
|
||||
sql.execute("DELETE FROM bobstate WHERE id=?;", paramsv!(self.id))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
self.next = next;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles the given message for the securejoin handshake for Bob.
|
||||
///
|
||||
/// If the message was not used for this handshake `None` is returned, otherwise the new
|
||||
/// stage is returned. Once [`BobHandshakeStage::Completed`] or
|
||||
/// [`BobHandshakeStage::Terminated`] are reached this [`BobState`] should be destroyed,
|
||||
/// further calling it will just result in the messages being unused by this handshake.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Under normal operation this should never return an error, regardless of what kind of
|
||||
/// message it is called with. Any errors therefore should be treated as fatal internal
|
||||
/// errors and this entire [`BobState`] should be thrown away as the state machine can
|
||||
/// no longer be considered consistent.
|
||||
async fn handle_message(
|
||||
pub async fn handle_message(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
@@ -304,17 +302,20 @@ impl BobState {
|
||||
} else {
|
||||
"Required encryption missing"
|
||||
};
|
||||
self.next = SecureJoinStep::Terminated;
|
||||
self.update_next(&context.sql, SecureJoinStep::Terminated)
|
||||
.await?;
|
||||
return Ok(Some(BobHandshakeStage::Terminated(reason)));
|
||||
}
|
||||
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.invite.contact_id())
|
||||
.await?
|
||||
{
|
||||
self.next = SecureJoinStep::Terminated;
|
||||
self.update_next(&context.sql, SecureJoinStep::Terminated)
|
||||
.await?;
|
||||
return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch")));
|
||||
}
|
||||
info!(context, "Fingerprint verified.",);
|
||||
self.next = SecureJoinStep::ContactConfirm;
|
||||
self.update_next(&context.sql, SecureJoinStep::ContactConfirm)
|
||||
.await?;
|
||||
self.send_handshake_message(context, BobHandshakeMsg::RequestWithAuth)
|
||||
.await?;
|
||||
Ok(Some(BobHandshakeStage::RequestWithAuthSent))
|
||||
@@ -362,7 +363,8 @@ impl BobState {
|
||||
if vg_expect_encrypted
|
||||
&& !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint()))
|
||||
{
|
||||
self.next = SecureJoinStep::Terminated;
|
||||
self.update_next(&context.sql, SecureJoinStep::Terminated)
|
||||
.await?;
|
||||
return Ok(Some(BobHandshakeStage::Terminated(
|
||||
"Contact confirm message not encrypted",
|
||||
)));
|
||||
@@ -394,7 +396,8 @@ impl BobState {
|
||||
// This is not an error affecting the protocol outcome.
|
||||
.ok();
|
||||
|
||||
self.next = SecureJoinStep::Completed;
|
||||
self.update_next(&context.sql, SecureJoinStep::Completed)
|
||||
.await?;
|
||||
Ok(Some(BobHandshakeStage::Completed))
|
||||
}
|
||||
|
||||
@@ -406,48 +409,60 @@ impl BobState {
|
||||
context: &Context,
|
||||
step: BobHandshakeMsg,
|
||||
) -> Result<(), SendMsgError> {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: Some(step.body_text(&self.invite)),
|
||||
hidden: true,
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
|
||||
|
||||
// Sends the step in Secure-Join header.
|
||||
msg.param
|
||||
.set(Param::Arg, step.securejoin_header(&self.invite));
|
||||
|
||||
match step {
|
||||
BobHandshakeMsg::Request => {
|
||||
// Sends the Secure-Join-Invitenumber header in mimefactory.rs.
|
||||
msg.param.set(Param::Arg2, self.invite.invitenumber());
|
||||
msg.force_plaintext();
|
||||
}
|
||||
BobHandshakeMsg::RequestWithAuth => {
|
||||
// Sends the Secure-Join-Auth header in mimefactory.rs.
|
||||
msg.param.set(Param::Arg2, self.invite.authcode());
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
}
|
||||
BobHandshakeMsg::ContactConfirmReceived => {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
|
||||
let bob_fp = SignedPublicKey::load_self(context).await?.fingerprint();
|
||||
msg.param.set(Param::Arg3, bob_fp.hex());
|
||||
|
||||
// Sends the grpid in the Secure-Join-Group header.
|
||||
if let QrInvite::Group { ref grpid, .. } = self.invite {
|
||||
msg.param.set(Param::Arg4, grpid);
|
||||
}
|
||||
|
||||
chat::send_msg(context, self.chat_id, &mut msg).await?;
|
||||
Ok(())
|
||||
send_handshake_message(context, &self.invite, self.chat_id, step).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the requested handshake message to Alice.
|
||||
///
|
||||
/// Same as [`BobState::send_handshake_message`] but this variation allows us to send this
|
||||
/// message before we create the state in [`BobState::start_protocol`].
|
||||
async fn send_handshake_message(
|
||||
context: &Context,
|
||||
invite: &QrInvite,
|
||||
chat_id: ChatId,
|
||||
step: BobHandshakeMsg,
|
||||
) -> Result<(), SendMsgError> {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: Some(step.body_text(invite)),
|
||||
hidden: true,
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
|
||||
|
||||
// Sends the step in Secure-Join header.
|
||||
msg.param.set(Param::Arg, step.securejoin_header(invite));
|
||||
|
||||
match step {
|
||||
BobHandshakeMsg::Request => {
|
||||
// Sends the Secure-Join-Invitenumber header in mimefactory.rs.
|
||||
msg.param.set(Param::Arg2, invite.invitenumber());
|
||||
msg.force_plaintext();
|
||||
}
|
||||
BobHandshakeMsg::RequestWithAuth => {
|
||||
// Sends the Secure-Join-Auth header in mimefactory.rs.
|
||||
msg.param.set(Param::Arg2, invite.authcode());
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
}
|
||||
BobHandshakeMsg::ContactConfirmReceived => {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
|
||||
let bob_fp = SignedPublicKey::load_self(context).await?.fingerprint();
|
||||
msg.param.set(Param::Arg3, bob_fp.hex());
|
||||
|
||||
// Sends the grpid in the Secure-Join-Group header.
|
||||
if let QrInvite::Group { ref grpid, .. } = invite {
|
||||
msg.param.set(Param::Arg4, grpid);
|
||||
}
|
||||
|
||||
chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Identifies the SecureJoin handshake messages Bob can send.
|
||||
enum BobHandshakeMsg {
|
||||
/// vc-request or vg-request
|
||||
@@ -492,8 +507,8 @@ impl BobHandshakeMsg {
|
||||
}
|
||||
|
||||
/// The next message expected by [`BobState`] in the setup-contact/secure-join protocol.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum SecureJoinStep {
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum SecureJoinStep {
|
||||
/// Expecting the auth-required message.
|
||||
///
|
||||
/// This corresponds to the `vc-auth-required` or `vg-auth-required` message of step 3d.
|
||||
@@ -533,3 +548,29 @@ impl SecureJoinStep {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for SecureJoinStep {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let num = match &self {
|
||||
SecureJoinStep::AuthRequired => 0,
|
||||
SecureJoinStep::ContactConfirm => 1,
|
||||
SecureJoinStep::Terminated => 2,
|
||||
SecureJoinStep::Completed => 3,
|
||||
};
|
||||
let val = rusqlite::types::Value::Integer(num);
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::FromSql for SecureJoinStep {
|
||||
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).and_then(|val| match val {
|
||||
0 => Ok(SecureJoinStep::AuthRequired),
|
||||
1 => Ok(SecureJoinStep::ContactConfirm),
|
||||
2 => Ok(SecureJoinStep::Terminated),
|
||||
3 => Ok(SecureJoinStep::Completed),
|
||||
_ => Err(rusqlite::types::FromSqlError::OutOfRange(val)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,22 +8,23 @@ use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{bail, Error, Result};
|
||||
|
||||
use crate::contact::ContactId;
|
||||
use crate::key::Fingerprint;
|
||||
use crate::qr::Qr;
|
||||
|
||||
/// Represents the data from a QR-code scan.
|
||||
///
|
||||
/// There are methods to conveniently access fields present in both variants.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum QrInvite {
|
||||
Contact {
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
fingerprint: Fingerprint,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
Group {
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
fingerprint: Fingerprint,
|
||||
name: String,
|
||||
grpid: String,
|
||||
@@ -37,7 +38,7 @@ impl QrInvite {
|
||||
///
|
||||
/// The actual QR-code contains a URL-encoded email address, but upon scanning this is
|
||||
/// translated to a contact ID.
|
||||
pub fn contact_id(&self) -> u32 {
|
||||
pub fn contact_id(&self) -> ContactId {
|
||||
match self {
|
||||
Self::Contact { contact_id, .. } | Self::Group { contact_id, .. } => *contact_id,
|
||||
}
|
||||
@@ -100,3 +101,22 @@ impl TryFrom<Qr> for QrInvite {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for QrInvite {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let json = serde_json::to_string(self)
|
||||
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
|
||||
let val = rusqlite::types::Value::Text(json);
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::FromSql for QrInvite {
|
||||
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
|
||||
String::column_result(value).and_then(|val| {
|
||||
serde_json::from_str(&val)
|
||||
.map_err(|err| rusqlite::types::FromSqlError::Other(Box::new(err)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
|
||||
ret += " [...]";
|
||||
}
|
||||
// redo escaping done by escape_message_footer_marks()
|
||||
ret.replace("\u{200B}", "")
|
||||
ret.replace('\u{200B}', "")
|
||||
}
|
||||
|
||||
/// Returns true if the line contains only whitespace.
|
||||
|
||||
366
src/smtp.rs
366
src/smtp.rs
@@ -4,14 +4,18 @@ pub mod send;
|
||||
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use async_smtp::smtp::client::net::ClientTlsParameters;
|
||||
use async_smtp::{error, smtp, EmailAddress, ServerAddress};
|
||||
use async_smtp::smtp::response::{Category, Code, Detail};
|
||||
use async_smtp::{smtp, EmailAddress, ServerAddress};
|
||||
|
||||
use crate::constants::DC_LP_AUTH_OAUTH2;
|
||||
use crate::events::EventType;
|
||||
use crate::job::Status;
|
||||
use crate::login_param::{
|
||||
dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam, Socks5Config,
|
||||
};
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::oauth2::dc_get_oauth2_access_token;
|
||||
use crate::provider::Socket;
|
||||
use crate::{context::Context, scheduler::connectivity::ConnectivityStore};
|
||||
@@ -19,28 +23,6 @@ use crate::{context::Context, scheduler::connectivity::ConnectivityStore};
|
||||
/// SMTP write and read timeout in seconds.
|
||||
const SMTP_TIMEOUT: u64 = 30;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Bad parameters")]
|
||||
BadParameters,
|
||||
#[error("Invalid login address {address}: {error}")]
|
||||
InvalidLoginAddress {
|
||||
address: String,
|
||||
#[source]
|
||||
error: error::Error,
|
||||
},
|
||||
#[error("SMTP failed to connect: {0}")]
|
||||
ConnectionFailure(#[source] smtp::error::Error),
|
||||
#[error("SMTP oauth2 error {address}")]
|
||||
Oauth2 { address: String },
|
||||
#[error("TLS error {0}")]
|
||||
Tls(#[from] async_native_tls::Error),
|
||||
#[error("{0}")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Smtp {
|
||||
transport: Option<smtp::SmtpTransport>,
|
||||
@@ -131,14 +113,11 @@ impl Smtp {
|
||||
}
|
||||
|
||||
if lp.server.is_empty() || lp.port == 0 {
|
||||
return Err(Error::BadParameters);
|
||||
bail!("bad connection parameters");
|
||||
}
|
||||
|
||||
let from =
|
||||
EmailAddress::new(addr.to_string()).map_err(|err| Error::InvalidLoginAddress {
|
||||
address: addr.to_string(),
|
||||
error: err,
|
||||
})?;
|
||||
let from = EmailAddress::new(addr.to_string())
|
||||
.with_context(|| format!("invalid login address {}", addr))?;
|
||||
|
||||
self.from = Some(from);
|
||||
|
||||
@@ -159,9 +138,7 @@ impl Smtp {
|
||||
let send_pw = &lp.password;
|
||||
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await?;
|
||||
if access_token.is_none() {
|
||||
return Err(Error::Oauth2 {
|
||||
address: addr.to_string(),
|
||||
});
|
||||
bail!("SMTP OAuth 2 error {}", addr);
|
||||
}
|
||||
let user = &lp.user;
|
||||
(
|
||||
@@ -205,9 +182,7 @@ impl Smtp {
|
||||
}
|
||||
|
||||
let mut trans = client.into_transport();
|
||||
if let Err(err) = trans.connect().await {
|
||||
return Err(Error::ConnectionFailure(err));
|
||||
}
|
||||
trans.connect().await.context("SMTP failed to connect")?;
|
||||
|
||||
self.transport = Some(trans);
|
||||
self.last_success = Some(SystemTime::now());
|
||||
@@ -220,3 +195,324 @@ impl Smtp {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to send a message.
|
||||
///
|
||||
/// Returns Status::Finished if sending the message should not be retried anymore,
|
||||
/// Status::RetryLater if sending should be postponed and Status::RetryNow if it is suspected that
|
||||
/// temporary failure is caused by stale connection, in which case a second attempt to send the
|
||||
/// same message may be done immediately.
|
||||
pub(crate) async fn smtp_send(
|
||||
context: &Context,
|
||||
recipients: &[async_smtp::EmailAddress],
|
||||
message: &str,
|
||||
smtp: &mut Smtp,
|
||||
msg_id: MsgId,
|
||||
rowid: i64,
|
||||
) -> Status {
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(context, "smtp-sending out mime message:");
|
||||
println!("{}", message);
|
||||
}
|
||||
|
||||
smtp.connectivity.set_working(context).await;
|
||||
|
||||
let send_result = smtp
|
||||
.send(context, recipients, message.as_bytes(), rowid)
|
||||
.await;
|
||||
smtp.last_send_error = send_result.as_ref().err().map(|e| e.to_string());
|
||||
|
||||
let status = match send_result {
|
||||
Err(crate::smtp::send::Error::SmtpSend(err)) => {
|
||||
// Remote error, retry later.
|
||||
info!(context, "SMTP failed to send: {:?}", &err);
|
||||
|
||||
let res = match err {
|
||||
async_smtp::smtp::error::Error::Permanent(ref response) => {
|
||||
// Workaround for incorrectly configured servers returning permanent errors
|
||||
// instead of temporary ones.
|
||||
let maybe_transient = match response.code {
|
||||
// Sometimes servers send a permanent error when actually it is a temporary error
|
||||
// For documentation see <https://tools.ietf.org/html/rfc3463>
|
||||
Code {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
..
|
||||
} => {
|
||||
// Ignore status code 5.5.0, see <https://support.delta.chat/t/every-other-message-gets-stuck/877/2>
|
||||
// Maybe incorrectly configured Postfix milter with "reject" instead of "tempfail", which returns
|
||||
// "550 5.5.0 Service unavailable" instead of "451 4.7.1 Service unavailable - try again later".
|
||||
//
|
||||
// Other enhanced status codes, such as Postfix
|
||||
// "550 5.1.1 <foobar@example.org>: Recipient address rejected: User unknown in local recipient table"
|
||||
// are not ignored.
|
||||
response.first_word() == Some("5.5.0")
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if maybe_transient {
|
||||
info!(context, "Permanent error that is likely to actually be transient, postponing retry for later");
|
||||
Status::RetryLater
|
||||
} else {
|
||||
info!(context, "Permanent error, message sending failed");
|
||||
// If we do not retry, add an info message to the chat.
|
||||
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
|
||||
// should definitely go here, because user has to open the link to
|
||||
// resume message sending.
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
}
|
||||
}
|
||||
async_smtp::smtp::error::Error::Transient(ref response) => {
|
||||
// We got a transient 4xx response from SMTP server.
|
||||
// Give some time until the server-side error maybe goes away.
|
||||
|
||||
if let Some(first_word) = response.first_word() {
|
||||
if first_word.ends_with(".1.1")
|
||||
|| first_word.ends_with(".1.2")
|
||||
|| first_word.ends_with(".1.3")
|
||||
{
|
||||
// Sometimes we receive transient errors that should be permanent.
|
||||
// Any extended smtp status codes like x.1.1, x.1.2 or x.1.3 that we
|
||||
// receive as a transient error are misconfigurations of the smtp server.
|
||||
// See <https://tools.ietf.org/html/rfc3463#section-3.2>
|
||||
info!(context, "Received extended status code {} for a transient error. This looks like a misconfigured smtp server, let's fail immediatly", first_word);
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Transient error with status code {}, postponing retry for later",
|
||||
first_word
|
||||
);
|
||||
Status::RetryLater
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Transient error without status code, postponing retry for later"
|
||||
);
|
||||
Status::RetryLater
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
info!(
|
||||
context,
|
||||
"Message sending failed without error returned by the server"
|
||||
);
|
||||
if smtp.has_maybe_stale_connection().await {
|
||||
info!(context, "Connection is probably stale, retry immediately");
|
||||
Status::RetryNow
|
||||
} else {
|
||||
info!(context, "Connection is not stale, retry later");
|
||||
Status::RetryLater
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// this clears last_success info
|
||||
info!(context, "Failed to send message over SMTP, disconnecting");
|
||||
smtp.disconnect().await;
|
||||
|
||||
res
|
||||
}
|
||||
Err(crate::smtp::send::Error::Envelope(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect().await;
|
||||
warn!(context, "SMTP job is invalid: {}", err);
|
||||
Status::Finished(Err(err.into()))
|
||||
}
|
||||
Err(crate::smtp::send::Error::NoTransport) => {
|
||||
// Should never happen.
|
||||
// It does not even make sense to disconnect here.
|
||||
error!(context, "SMTP job failed because SMTP has no transport");
|
||||
Status::Finished(Err(format_err!("SMTP has not transport")))
|
||||
}
|
||||
Err(crate::smtp::send::Error::Other(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect().await;
|
||||
warn!(context, "unable to load job: {}", err);
|
||||
Status::Finished(Err(err))
|
||||
}
|
||||
Ok(()) => Status::Finished(Ok(())),
|
||||
};
|
||||
|
||||
if let Status::Finished(Err(err)) = &status {
|
||||
// We couldn't send the message, so mark it as failed
|
||||
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
|
||||
}
|
||||
status
|
||||
}
|
||||
|
||||
/// Sends message identified by `smtp` table rowid over SMTP connection.
|
||||
///
|
||||
/// Removes row if the message should not be retried, otherwise increments retry count.
|
||||
pub(crate) async fn send_msg_to_smtp(
|
||||
context: &Context,
|
||||
smtp: &mut Smtp,
|
||||
rowid: i64,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Err(err) = smtp
|
||||
.connect_configured(context)
|
||||
.await
|
||||
.context("SMTP connection failure")
|
||||
{
|
||||
smtp.last_send_error = Some(format!("{:#}", err));
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
// Increase retry count as soon as we have an SMTP connection. This ensures that the message is
|
||||
// eventually removed from the queue by exceeding retry limit even in case of an error that
|
||||
// keeps happening early in the message sending code, e.g. failure to read the message from the
|
||||
// database.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE smtp SET retries=retries+1 WHERE id=?",
|
||||
paramsv![rowid],
|
||||
)
|
||||
.await
|
||||
.context("failed to update retries count")?;
|
||||
|
||||
let (body, recipients, msg_id, retries) = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?",
|
||||
paramsv![rowid],
|
||||
|row| {
|
||||
let mime: String = row.get(0)?;
|
||||
let recipients: String = row.get(1)?;
|
||||
let msg_id: MsgId = row.get(2)?;
|
||||
let retries: i64 = row.get(3)?;
|
||||
Ok((mime, recipients, msg_id, retries))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if retries > 6 {
|
||||
message::set_msg_failed(
|
||||
context,
|
||||
msg_id,
|
||||
Some("Number of retries exceeded the limit."),
|
||||
)
|
||||
.await;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
|
||||
.await
|
||||
.context("failed to remove message with exceeded retry limit from smtp table")?;
|
||||
bail!("Number of retries exceeded the limit");
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"Retry number {} to send message {} over SMTP", retries, msg_id
|
||||
);
|
||||
|
||||
let recipients_list = recipients
|
||||
.split(' ')
|
||||
.filter_map(
|
||||
|addr| match async_smtp::EmailAddress::new(addr.to_string()) {
|
||||
Ok(addr) => Some(addr),
|
||||
Err(err) => {
|
||||
warn!(context, "invalid recipient: {} {:?}", addr, err);
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// If there is a msg-id and it does not exist in the db, cancel sending. this happens if
|
||||
// dc_delete_msgs() was called before the generated mime was sent out.
|
||||
if !message::exists(context, msg_id)
|
||||
.await
|
||||
.with_context(|| format!("failed to check message {} existence", msg_id))?
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
"Sending of message {} was cancelled by the user.", msg_id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let status = match smtp_send(
|
||||
context,
|
||||
&recipients_list,
|
||||
body.as_str(),
|
||||
smtp,
|
||||
msg_id,
|
||||
rowid,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Status::RetryNow => {
|
||||
// Do a single retry immediately without increasing retry counter in case of stale
|
||||
// connection.
|
||||
info!(context, "Doing immediate retry to send message.");
|
||||
|
||||
// smtp_send just closed stale SMTP connection, reconnect and try again.
|
||||
if let Err(err) = smtp
|
||||
.connect_configured(context)
|
||||
.await
|
||||
.context("failed to reopen stale SMTP connection")
|
||||
{
|
||||
smtp.last_send_error = Some(format!("{:#}", err));
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
smtp_send(
|
||||
context,
|
||||
&recipients_list,
|
||||
body.as_str(),
|
||||
smtp,
|
||||
msg_id,
|
||||
rowid,
|
||||
)
|
||||
.await
|
||||
}
|
||||
status => status,
|
||||
};
|
||||
match status {
|
||||
Status::Finished(res) => {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
|
||||
.await?;
|
||||
if res.is_ok() {
|
||||
msg_id.set_delivered(context).await?;
|
||||
}
|
||||
res
|
||||
}
|
||||
Status::RetryNow | Status::RetryLater => Err(format_err!("Retry")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to send all messages currently in `smtp` table.
|
||||
///
|
||||
/// 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,
|
||||
) -> anyhow::Result<()> {
|
||||
context.send_sync_msg().await?; // Add sync message to the end of the queue if needed.
|
||||
let rowids = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM smtp ORDER BY id ASC",
|
||||
paramsv![],
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
Ok(rowid)
|
||||
},
|
||||
|rowids| {
|
||||
rowids
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
for rowid in rowids {
|
||||
if let Err(err) = send_msg_to_smtp(context, connection, rowid).await {
|
||||
info!(context, "Failed to send message over SMTP: {:#}.", err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ impl Smtp {
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
recipients: Vec<EmailAddress>,
|
||||
message: Vec<u8>,
|
||||
job_id: u32,
|
||||
recipients: &[EmailAddress],
|
||||
message: &[u8],
|
||||
rowid: i64,
|
||||
) -> Result<()> {
|
||||
let message_len_bytes = message.len();
|
||||
|
||||
@@ -41,7 +41,7 @@ impl Smtp {
|
||||
}
|
||||
}
|
||||
|
||||
for recipients_chunk in recipients.chunks(chunk_size).into_iter() {
|
||||
for recipients_chunk in recipients.chunks(chunk_size) {
|
||||
let recipients_display = recipients_chunk
|
||||
.iter()
|
||||
.map(|x| x.as_ref())
|
||||
@@ -52,8 +52,8 @@ impl Smtp {
|
||||
.map_err(Error::Envelope)?;
|
||||
let mail = SendableEmail::new(
|
||||
envelope,
|
||||
format!("{}", job_id), // only used for internal logging
|
||||
&message,
|
||||
rowid.to_string(), // only used for internal logging
|
||||
message,
|
||||
);
|
||||
|
||||
if let Some(ref mut transport) = self.transport {
|
||||
|
||||
456
src/sql.rs
456
src/sql.rs
@@ -3,22 +3,23 @@
|
||||
use async_std::path::Path;
|
||||
use async_std::sync::RwLock;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::TryFrom;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::prelude::*;
|
||||
use rusqlite::OpenFlags;
|
||||
use rusqlite::{config::DbConfig, Connection, OpenFlags};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Viewtype, DC_CHAT_ID_TRASH};
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_delete_file, time};
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{deduplicate_peerstates, Peerstate};
|
||||
use crate::stock_str;
|
||||
@@ -38,20 +39,53 @@ mod migrations;
|
||||
/// A wrapper around the underlying Sqlite3 object.
|
||||
#[derive(Debug)]
|
||||
pub struct Sql {
|
||||
pool: RwLock<Option<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>>,
|
||||
}
|
||||
/// Database file path
|
||||
pub(crate) dbfile: PathBuf,
|
||||
|
||||
impl Default for Sql {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pool: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
pool: RwLock<Option<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>>,
|
||||
|
||||
/// None if the database is not open, true if it is open with passphrase and false if it is
|
||||
/// open without a passphrase.
|
||||
is_encrypted: RwLock<Option<bool>>,
|
||||
|
||||
pub(crate) config_cache: RwLock<HashMap<String, Option<String>>>,
|
||||
}
|
||||
|
||||
impl Sql {
|
||||
pub fn new() -> Sql {
|
||||
Self::default()
|
||||
pub fn new(dbfile: PathBuf) -> Sql {
|
||||
Self {
|
||||
dbfile,
|
||||
pool: Default::default(),
|
||||
is_encrypted: Default::default(),
|
||||
config_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests SQLCipher passphrase.
|
||||
///
|
||||
/// Returns true if passphrase is correct, i.e. the database is new or can be unlocked with
|
||||
/// this passphrase, and false if the database is already encrypted with another passphrase or
|
||||
/// corrupted.
|
||||
///
|
||||
/// Fails if database is already open.
|
||||
pub async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
|
||||
if self.is_open().await {
|
||||
bail!("Database is already opened.");
|
||||
}
|
||||
|
||||
// Hold the lock to prevent other thread from opening the database.
|
||||
let _lock = self.pool.write().await;
|
||||
|
||||
// Test that the key is correct using a single connection.
|
||||
let connection = Connection::open(&self.dbfile)?;
|
||||
connection
|
||||
.pragma_update(None, "key", &passphrase)
|
||||
.context("failed to set PRAGMA key")?;
|
||||
let key_is_correct = connection
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.is_ok();
|
||||
|
||||
Ok(key_is_correct)
|
||||
}
|
||||
|
||||
/// Checks if there is currently a connection to the underlying Sqlite database.
|
||||
@@ -59,37 +93,98 @@ impl Sql {
|
||||
self.pool.read().await.is_some()
|
||||
}
|
||||
|
||||
/// Returns true if the database is encrypted.
|
||||
///
|
||||
/// If database is not open, returns `None`.
|
||||
pub(crate) async fn is_encrypted(&self) -> Option<bool> {
|
||||
*self.is_encrypted.read().await
|
||||
}
|
||||
|
||||
/// Closes all underlying Sqlite connections.
|
||||
pub async fn close(&self) {
|
||||
async fn close(&self) {
|
||||
let _ = self.pool.write().await.take();
|
||||
// drop closes the connection
|
||||
}
|
||||
|
||||
pub fn new_pool(
|
||||
/// Exports the database to a separate file with the given passphrase.
|
||||
///
|
||||
/// Set passphrase to empty string to export the database unencrypted.
|
||||
pub(crate) async fn export(&self, path: &Path, passphrase: String) -> Result<()> {
|
||||
let path_str = path
|
||||
.to_str()
|
||||
.with_context(|| format!("path {:?} is not valid unicode", path))?;
|
||||
let conn = self.get_conn().await?;
|
||||
conn.execute(
|
||||
"ATTACH DATABASE ? AS backup KEY ?",
|
||||
paramsv![path_str, passphrase],
|
||||
)
|
||||
.context("failed to attach backup database")?;
|
||||
let res = conn
|
||||
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
|
||||
.context("failed to export to attached backup database");
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database")?;
|
||||
res?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Imports the database from a separate file with the given passphrase.
|
||||
pub(crate) async fn import(&self, path: &Path, passphrase: String) -> Result<()> {
|
||||
let path_str = path
|
||||
.to_str()
|
||||
.with_context(|| format!("path {:?} is not valid unicode", path))?;
|
||||
let conn = self.get_conn().await?;
|
||||
|
||||
// Reset the database without reopening it. We don't want to reopen the database because we
|
||||
// don't have main database passphrase at this point.
|
||||
// See <https://sqlite.org/c3ref/c_dbconfig_enable_fkey.html> for documentation.
|
||||
// Without resetting import may fail due to existing tables.
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)
|
||||
.context("failed to set SQLITE_DBCONFIG_RESET_DATABASE")?;
|
||||
conn.execute("VACUUM", [])
|
||||
.context("failed to vacuum the database")?;
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)
|
||||
.context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE")?;
|
||||
|
||||
conn.execute(
|
||||
"ATTACH DATABASE ? AS backup KEY ?",
|
||||
paramsv![path_str, passphrase],
|
||||
)
|
||||
.context("failed to attach backup database")?;
|
||||
let res = conn
|
||||
.query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| {
|
||||
Ok(())
|
||||
})
|
||||
.context("failed to import from attached backup database");
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database")?;
|
||||
res?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_pool(
|
||||
dbfile: &Path,
|
||||
readonly: bool,
|
||||
) -> anyhow::Result<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>> {
|
||||
passphrase: String,
|
||||
) -> Result<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>> {
|
||||
let mut open_flags = OpenFlags::SQLITE_OPEN_NO_MUTEX;
|
||||
if readonly {
|
||||
open_flags.insert(OpenFlags::SQLITE_OPEN_READ_ONLY);
|
||||
} else {
|
||||
open_flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE);
|
||||
open_flags.insert(OpenFlags::SQLITE_OPEN_CREATE);
|
||||
}
|
||||
open_flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE);
|
||||
open_flags.insert(OpenFlags::SQLITE_OPEN_CREATE);
|
||||
|
||||
// this actually creates min_idle database handles just now.
|
||||
// therefore, with_init() must not try to modify the database as otherwise
|
||||
// we easily get busy-errors (eg. table-creation, journal_mode etc. should be done on only one handle)
|
||||
let mgr = r2d2_sqlite::SqliteConnectionManager::file(dbfile)
|
||||
.with_flags(open_flags)
|
||||
.with_init(|c| {
|
||||
.with_init(move |c| {
|
||||
c.execute_batch(&format!(
|
||||
"PRAGMA secure_delete=on;
|
||||
"PRAGMA cipher_memory_security = OFF; -- Too slow on Android
|
||||
PRAGMA secure_delete=on;
|
||||
PRAGMA busy_timeout = {};
|
||||
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
|
||||
",
|
||||
Duration::from_secs(10).as_millis()
|
||||
))?;
|
||||
c.pragma_update(None, "key", passphrase.clone())?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
@@ -102,114 +197,125 @@ impl Sql {
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> {
|
||||
*self.pool.write().await = Some(Self::new_pool(dbfile, passphrase.to_string())?);
|
||||
|
||||
{
|
||||
let conn = self.get_conn().await?;
|
||||
|
||||
// Try to enable auto_vacuum. This will only be
|
||||
// applied if the database is new or after successful
|
||||
// VACUUM, which usually happens before backup export.
|
||||
// When auto_vacuum is INCREMENTAL, it is possible to
|
||||
// use PRAGMA incremental_vacuum to return unused
|
||||
// database pages to the filesystem.
|
||||
conn.pragma_update(None, "auto_vacuum", &"INCREMENTAL".to_string())?;
|
||||
|
||||
// journal_mode is persisted, it is sufficient to change it only for one handle.
|
||||
conn.pragma_update(None, "journal_mode", &"WAL".to_string())?;
|
||||
|
||||
// Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode.
|
||||
conn.pragma_update(None, "synchronous", &"NORMAL".to_string())?;
|
||||
}
|
||||
|
||||
self.run_migrations(context).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_migrations(&self, context: &Context) -> Result<()> {
|
||||
// (1) update low-level database structure.
|
||||
// this should be done before updates that use high-level objects that
|
||||
// rely themselves on the low-level structure.
|
||||
|
||||
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
|
||||
migrations::run(context, self)
|
||||
.await
|
||||
.context("failed to run migrations")?;
|
||||
|
||||
// (2) updates that require high-level objects
|
||||
// the structure is complete now and all objects are usable
|
||||
|
||||
if recalc_fingerprints {
|
||||
info!(context, "[migration] recalc fingerprints");
|
||||
let addrs = self
|
||||
.query_map(
|
||||
"SELECT addr FROM acpeerstates;",
|
||||
paramsv![],
|
||||
|row| row.get::<_, String>(0),
|
||||
|addrs| {
|
||||
addrs
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
for addr in &addrs {
|
||||
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
|
||||
peerstate.recalc_fingerprint();
|
||||
peerstate.save_to_db(self, false).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if update_icons {
|
||||
update_saved_messages_icon(context).await?;
|
||||
update_device_icon(context).await?;
|
||||
}
|
||||
|
||||
if disable_server_delete {
|
||||
// We now always watch all folders and delete messages there if delete_server is enabled.
|
||||
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
|
||||
if context.get_config_delete_server_after().await?.is_some() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(stock_str::delete_server_turned_off(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
context
|
||||
.set_config(Config::DeleteServerAfter, Some("0"))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if recode_avatar {
|
||||
if let Some(avatar) = context.get_config(Config::Selfavatar).await? {
|
||||
let mut blob = BlobObject::new_from_path(context, avatar.as_ref()).await?;
|
||||
match blob.recode_to_avatar_size(context).await {
|
||||
Ok(()) => {
|
||||
context
|
||||
.set_config(Config::Selfavatar, Some(&avatar))
|
||||
.await?
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
|
||||
context.set_config(Config::Selfavatar, None).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Opens the provided database and runs any necessary migrations.
|
||||
/// If a database is already open, this will return an error.
|
||||
pub async fn open(
|
||||
&self,
|
||||
context: &Context,
|
||||
dbfile: &Path,
|
||||
readonly: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
pub async fn open(&self, context: &Context, passphrase: String) -> Result<()> {
|
||||
if self.is_open().await {
|
||||
error!(
|
||||
context,
|
||||
"Cannot open, database \"{:?}\" already opened.", dbfile,
|
||||
"Cannot open, database \"{:?}\" already opened.", self.dbfile,
|
||||
);
|
||||
bail!("SQL database is already opened.");
|
||||
}
|
||||
|
||||
*self.pool.write().await = Some(Self::new_pool(dbfile, readonly)?);
|
||||
|
||||
if !readonly {
|
||||
{
|
||||
let conn = self.get_conn().await?;
|
||||
|
||||
// Try to enable auto_vacuum. This will only be
|
||||
// applied if the database is new or after successful
|
||||
// VACUUM, which usually happens before backup export.
|
||||
// When auto_vacuum is INCREMENTAL, it is possible to
|
||||
// use PRAGMA incremental_vacuum to return unused
|
||||
// database pages to the filesystem.
|
||||
conn.pragma_update(None, "auto_vacuum", &"INCREMENTAL".to_string())?;
|
||||
|
||||
// journal_mode is persisted, it is sufficient to change it only for one handle.
|
||||
conn.pragma_update(None, "journal_mode", &"WAL".to_string())?;
|
||||
|
||||
// Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode.
|
||||
conn.pragma_update(None, "synchronous", &"NORMAL".to_string())?;
|
||||
}
|
||||
|
||||
// (1) update low-level database structure.
|
||||
// this should be done before updates that use high-level objects that
|
||||
// rely themselves on the low-level structure.
|
||||
|
||||
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
|
||||
migrations::run(context, self).await?;
|
||||
|
||||
// (2) updates that require high-level objects
|
||||
// the structure is complete now and all objects are usable
|
||||
|
||||
if recalc_fingerprints {
|
||||
info!(context, "[migration] recalc fingerprints");
|
||||
let addrs = self
|
||||
.query_map(
|
||||
"SELECT addr FROM acpeerstates;",
|
||||
paramsv![],
|
||||
|row| row.get::<_, String>(0),
|
||||
|addrs| {
|
||||
addrs
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
for addr in &addrs {
|
||||
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
|
||||
peerstate.recalc_fingerprint();
|
||||
peerstate.save_to_db(self, false).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if update_icons {
|
||||
update_saved_messages_icon(context).await?;
|
||||
update_device_icon(context).await?;
|
||||
}
|
||||
|
||||
if disable_server_delete {
|
||||
// We now always watch all folders and delete messages there if delete_server is enabled.
|
||||
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
|
||||
if context.get_config_delete_server_after().await?.is_some() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(stock_str::delete_server_turned_off(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
context
|
||||
.set_config(Config::DeleteServerAfter, Some("0"))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if recode_avatar {
|
||||
if let Some(avatar) = context.get_config(Config::Selfavatar).await? {
|
||||
let mut blob = BlobObject::new_from_path(context, avatar.as_ref()).await?;
|
||||
match blob.recode_to_avatar_size(context).await {
|
||||
Ok(()) => {
|
||||
context
|
||||
.set_config(Config::Selfavatar, Some(&avatar))
|
||||
.await?
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
|
||||
context.set_config(Config::Selfavatar, None).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let passphrase_nonempty = !passphrase.is_empty();
|
||||
if let Err(err) = self.try_open(context, &self.dbfile, passphrase).await {
|
||||
self.close().await;
|
||||
Err(err)
|
||||
} else {
|
||||
info!(context, "Opened database {:?}.", self.dbfile);
|
||||
*self.is_encrypted.write().await = Some(passphrase_nonempty);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
info!(context, "Opened database {:?}.", dbfile);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute the given query, returning the number of affected rows.
|
||||
@@ -228,10 +334,10 @@ impl Sql {
|
||||
&self,
|
||||
query: impl AsRef<str>,
|
||||
params: impl rusqlite::Params,
|
||||
) -> anyhow::Result<usize> {
|
||||
) -> Result<i64> {
|
||||
let conn = self.get_conn().await?;
|
||||
conn.execute(query.as_ref(), params)?;
|
||||
Ok(usize::try_from(conn.last_insert_rowid())?)
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
/// Prepares and executes the statement and maps a function over the resulting rows.
|
||||
@@ -260,9 +366,7 @@ impl Sql {
|
||||
&self,
|
||||
) -> Result<r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>> {
|
||||
let lock = self.pool.read().await;
|
||||
let pool = lock
|
||||
.as_ref()
|
||||
.ok_or_else(|| format_err!("No SQL connection"))?;
|
||||
let pool = lock.as_ref().context("no SQL connection")?;
|
||||
let conn = pool.get()?;
|
||||
|
||||
Ok(conn)
|
||||
@@ -396,6 +500,8 @@ impl Sql {
|
||||
/// will already have been logged.
|
||||
pub async fn set_raw_config(&self, key: impl AsRef<str>, value: Option<&str>) -> Result<()> {
|
||||
let key = key.as_ref();
|
||||
|
||||
let mut lock = self.config_cache.write().await;
|
||||
if let Some(value) = value {
|
||||
let exists = self
|
||||
.exists(
|
||||
@@ -421,12 +527,23 @@ impl Sql {
|
||||
self.execute("DELETE FROM config WHERE keyname=?;", paramsv![key])
|
||||
.await?;
|
||||
}
|
||||
lock.insert(key.to_string(), value.map(|s| s.to_string()));
|
||||
drop(lock);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get configuration options from the database.
|
||||
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Result<Option<String>> {
|
||||
let lock = self.config_cache.read().await;
|
||||
let cached = lock.get(key.as_ref()).cloned();
|
||||
drop(lock);
|
||||
|
||||
if let Some(c) = cached {
|
||||
return Ok(c);
|
||||
}
|
||||
|
||||
let mut lock = self.config_cache.write().await;
|
||||
let value = self
|
||||
.query_get_value(
|
||||
"SELECT value FROM config WHERE keyname=?;",
|
||||
@@ -434,6 +551,8 @@ impl Sql {
|
||||
)
|
||||
.await
|
||||
.context(format!("failed to fetch raw config: {}", key.as_ref()))?;
|
||||
lock.insert(key.as_ref().to_string(), value.clone());
|
||||
drop(lock);
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
@@ -472,6 +591,11 @@ impl Sql {
|
||||
.await
|
||||
.map(|s| s.and_then(|r| r.parse().ok()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "internals")]
|
||||
pub fn config_cache(&self) -> &RwLock<HashMap<String, Option<String>>> {
|
||||
&self.config_cache
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
@@ -686,7 +810,7 @@ async fn maybe_add_from_param(
|
||||
async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
sql.execute(
|
||||
"DELETE FROM msgs
|
||||
WHERE (chat_id=? OR hidden)
|
||||
WHERE chat_id=?
|
||||
AND NOT EXISTS (
|
||||
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
|
||||
)",
|
||||
@@ -696,6 +820,19 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to return comma-separated sequence of `?` chars.
|
||||
///
|
||||
/// Use this together with [`rusqlite::ParamsFromIter`] to use dynamically generated
|
||||
/// parameter lists.
|
||||
pub fn repeat_vars(count: usize) -> Result<String> {
|
||||
if count == 0 {
|
||||
bail!("Must have at least one repeat variable");
|
||||
}
|
||||
let mut s = "?,".repeat(count);
|
||||
s.pop(); // Remove trailing comma
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_std::channel;
|
||||
@@ -787,7 +924,7 @@ mod tests {
|
||||
|
||||
t.sql.close().await;
|
||||
housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed
|
||||
t.sql.open(&t, t.get_dbfile(), false).await.unwrap();
|
||||
t.sql.open(&t, "".to_string()).await.unwrap();
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
|
||||
@@ -805,6 +942,23 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test for a bug where housekeeping deleted drafts since their
|
||||
/// `hidden` flag is set.
|
||||
#[async_std::test]
|
||||
async fn test_housekeeping_dont_delete_drafts() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
|
||||
let mut new_draft = Message::new(Viewtype::Text);
|
||||
new_draft.set_text(Some("This is my draft".to_string()));
|
||||
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
|
||||
|
||||
housekeeping(&t).await.unwrap();
|
||||
|
||||
let loaded_draft = chat.id.get_draft(&t).await.unwrap();
|
||||
assert_eq!(loaded_draft.unwrap().text.unwrap(), "This is my draft");
|
||||
}
|
||||
|
||||
/// Regression test.
|
||||
///
|
||||
/// Previously the code checking for existence of `config` table
|
||||
@@ -827,14 +981,14 @@ mod tests {
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new();
|
||||
let sql = Sql::new(dbfile.into());
|
||||
|
||||
// Create database with all the tables.
|
||||
sql.open(&t, dbfile.as_ref(), false).await.unwrap();
|
||||
sql.open(&t, "".to_string()).await.unwrap();
|
||||
sql.close().await;
|
||||
|
||||
// Reopen the database
|
||||
sql.open(&t, dbfile.as_ref(), false).await?;
|
||||
sql.open(&t, "".to_string()).await?;
|
||||
sql.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||
paramsv!("foo", "bar"),
|
||||
@@ -887,4 +1041,36 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile.clone().into());
|
||||
|
||||
sql.check_passphrase("foo".to_string()).await?;
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database first time")?;
|
||||
sql.close().await;
|
||||
|
||||
// Reopen the database
|
||||
let sql = Sql::new(dbfile.into());
|
||||
|
||||
// Test that we can't open encrypted database without a passphrase.
|
||||
assert!(sql.open(&t, "".to_string()).await.is_err());
|
||||
|
||||
// Now open the database with passpharse, it should succeed.
|
||||
sql.check_passphrase("foo".to_string()).await?;
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Migrations module.
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::ShowEmails;
|
||||
@@ -19,7 +19,11 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
|
||||
let mut exists_before_update = false;
|
||||
let mut dbversion_before_update = DBVERSION;
|
||||
|
||||
if !sql.table_exists("config").await? {
|
||||
if !sql
|
||||
.table_exists("config")
|
||||
.await
|
||||
.context("failed to check if config table exists")?
|
||||
{
|
||||
info!(context, "First time init: creating tables",);
|
||||
sql.transaction(move |transaction| {
|
||||
transaction.execute_batch(TABLES)?;
|
||||
@@ -32,6 +36,13 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut lock = context.sql.config_cache.write().await;
|
||||
lock.insert(
|
||||
VERSION_CFG.to_string(),
|
||||
Some(format!("{}", dbversion_before_update)),
|
||||
);
|
||||
drop(lock);
|
||||
} else {
|
||||
exists_before_update = true;
|
||||
dbversion_before_update = sql
|
||||
@@ -535,6 +546,68 @@ DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 83 {
|
||||
info!(context, "[migration] v83");
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE imap_sync
|
||||
ADD COLUMN modseq -- Highest modification sequence
|
||||
INTEGER DEFAULT 0",
|
||||
83,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 84 {
|
||||
info!(context, "[migration] v84");
|
||||
sql.execute_migration(
|
||||
r#"CREATE TABLE msgs_status_updates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
msg_id INTEGER,
|
||||
update_item TEXT DEFAULT '',
|
||||
update_item_read INTEGER DEFAULT 0);
|
||||
CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);"#,
|
||||
84,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 85 {
|
||||
info!(context, "[migration] v85");
|
||||
sql.execute_migration(
|
||||
r#"CREATE TABLE smtp (
|
||||
id INTEGER PRIMARY KEY,
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID
|
||||
mime TEXT NOT NULL, -- SMTP payload
|
||||
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
|
||||
recipients TEXT NOT NULL, -- List of recipients separated by space
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the messsage
|
||||
);
|
||||
CREATE INDEX smtp_messageid ON imap(rfc724_mid);
|
||||
"#,
|
||||
85,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 86 {
|
||||
info!(context, "[migration] v86");
|
||||
sql.execute_migration(
|
||||
r#"CREATE TABLE bobstate (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
invite TEXT NOT NULL,
|
||||
next_step INTEGER NOT NULL,
|
||||
chat_id INTEGER NOT NULL
|
||||
);"#,
|
||||
86,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 87 {
|
||||
info!(context, "[migration] v87");
|
||||
// the index is used to speed up delete_expired_messages()
|
||||
sql.execute_migration(
|
||||
"CREATE INDEX IF NOT EXISTS msgs_index8 ON msgs (ephemeral_timestamp);",
|
||||
87,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((
|
||||
recalc_fingerprints,
|
||||
@@ -562,7 +635,12 @@ impl Sql {
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
.await
|
||||
.with_context(|| format!("execute_migration failed for version {}", version))?;
|
||||
|
||||
let mut lock = self.config_cache.write().await;
|
||||
lock.insert(VERSION_CFG.to_string(), Some(format!("{}", version)));
|
||||
drop(lock);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -168,6 +168,14 @@ CREATE TABLE tokens (
|
||||
timestamp INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
-- The currently running securejoin protocols, joiner-side.
|
||||
-- CREATE TABLE bobstate (
|
||||
-- id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
-- invite TEXT NOT NULL,
|
||||
-- next_step INTEGER NOT NULL,
|
||||
-- chat_id INTEGER NOT NULL
|
||||
-- );
|
||||
|
||||
CREATE TABLE locations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
latitude REAL DEFAULT 0.0,
|
||||
|
||||
@@ -10,11 +10,11 @@ use strum_macros::EnumProperty;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::constants::DC_CONTACT_ID_SELF;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::dc_timestamp_to_str;
|
||||
use crate::message::Message;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use humansize::{file_size_opts, FileSize};
|
||||
|
||||
@@ -53,9 +53,6 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "File"))]
|
||||
File = 12,
|
||||
|
||||
#[strum(props(fallback = "Sent with my Delta Chat Messenger: https://delta.chat"))]
|
||||
StatusLine = 13,
|
||||
|
||||
#[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\"."))]
|
||||
MsgGrpName = 15,
|
||||
|
||||
@@ -392,7 +389,7 @@ trait StockStringMods: AsRef<str> + Sized {
|
||||
fn action_by_contact<'a>(
|
||||
self,
|
||||
context: &'a Context,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
) -> Pin<Box<dyn Future<Output = String> + Send + 'a>>
|
||||
where
|
||||
Self: Send + 'a,
|
||||
@@ -455,17 +452,12 @@ pub(crate) async fn file(context: &Context) -> String {
|
||||
translated(context, StockMessage::File).await
|
||||
}
|
||||
|
||||
/// Stock string: `Sent with my Delta Chat Messenger: https://delta.chat`.
|
||||
pub(crate) async fn status_line(context: &Context) -> String {
|
||||
translated(context, StockMessage::StatusLine).await
|
||||
}
|
||||
|
||||
/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
|
||||
pub(crate) async fn msg_grp_name(
|
||||
context: &Context,
|
||||
from_group: impl AsRef<str>,
|
||||
to_group: impl AsRef<str>,
|
||||
by_contact: u32,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
translated(context, StockMessage::MsgGrpName)
|
||||
.await
|
||||
@@ -476,7 +468,7 @@ pub(crate) async fn msg_grp_name(
|
||||
}
|
||||
|
||||
/// Stock string: `Group image changed.`.
|
||||
pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: u32) -> String {
|
||||
pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
|
||||
translated(context, StockMessage::MsgGrpImgChanged)
|
||||
.await
|
||||
.action_by_contact(context, by_contact)
|
||||
@@ -490,7 +482,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: u32) -> S
|
||||
pub(crate) async fn msg_add_member(
|
||||
context: &Context,
|
||||
added_member_addr: impl AsRef<str>,
|
||||
by_contact: u32,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
let addr = added_member_addr.as_ref();
|
||||
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
|
||||
@@ -514,7 +506,7 @@ pub(crate) async fn msg_add_member(
|
||||
pub(crate) async fn msg_del_member(
|
||||
context: &Context,
|
||||
removed_member_addr: impl AsRef<str>,
|
||||
by_contact: u32,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
let addr = removed_member_addr.as_ref();
|
||||
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
|
||||
@@ -532,7 +524,7 @@ pub(crate) async fn msg_del_member(
|
||||
}
|
||||
|
||||
/// Stock string: `Group left.`.
|
||||
pub(crate) async fn msg_group_left(context: &Context, by_contact: u32) -> String {
|
||||
pub(crate) async fn msg_group_left(context: &Context, by_contact: ContactId) -> String {
|
||||
translated(context, StockMessage::MsgGroupLeft)
|
||||
.await
|
||||
.action_by_contact(context, by_contact)
|
||||
@@ -582,7 +574,7 @@ pub(crate) async fn read_rcpt_mail_body(context: &Context, message: impl AsRef<s
|
||||
}
|
||||
|
||||
/// Stock string: `Group image deleted.`.
|
||||
pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: u32) -> String {
|
||||
pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
|
||||
translated(context, StockMessage::MsgGrpImgDeleted)
|
||||
.await
|
||||
.action_by_contact(context, by_contact)
|
||||
@@ -595,7 +587,10 @@ pub(crate) async fn e2e_preferred(context: &Context) -> String {
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`.
|
||||
pub(crate) async fn secure_join_started(context: &Context, inviter_contact_id: u32) -> String {
|
||||
pub(crate) async fn secure_join_started(
|
||||
context: &Context,
|
||||
inviter_contact_id: ContactId,
|
||||
) -> String {
|
||||
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
|
||||
translated(context, StockMessage::SecureJoinStarted)
|
||||
.await
|
||||
@@ -610,7 +605,7 @@ pub(crate) async fn secure_join_started(context: &Context, inviter_contact_id: u
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s replied, waiting for being added to the group…`.
|
||||
pub(crate) async fn secure_join_replies(context: &Context, contact_id: u32) -> String {
|
||||
pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
translated(context, StockMessage::SecureJoinReplies)
|
||||
.await
|
||||
@@ -644,20 +639,19 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s verified.`.
|
||||
pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef<str>) -> String {
|
||||
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
|
||||
let addr = contact.get_name_n_addr();
|
||||
translated(context, StockMessage::ContactVerified)
|
||||
.await
|
||||
.replace1(contact_addr)
|
||||
.replace1(addr)
|
||||
}
|
||||
|
||||
/// Stock string: `Cannot verify %1$s`.
|
||||
pub(crate) async fn contact_not_verified(
|
||||
context: &Context,
|
||||
contact_addr: impl AsRef<str>,
|
||||
) -> String {
|
||||
pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String {
|
||||
let addr = contact.get_name_n_addr();
|
||||
translated(context, StockMessage::ContactNotVerified)
|
||||
.await
|
||||
.replace1(contact_addr)
|
||||
.replace1(addr)
|
||||
}
|
||||
|
||||
/// Stock string: `Changed setup for %1$s`.
|
||||
@@ -727,7 +721,7 @@ pub(crate) async fn msg_location_enabled(context: &Context) -> String {
|
||||
}
|
||||
|
||||
/// Stock string: `Location streaming enabled by ...`.
|
||||
pub(crate) async fn msg_location_enabled_by(context: &Context, contact: u32) -> String {
|
||||
pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
|
||||
translated(context, StockMessage::MsgLocationEnabled)
|
||||
.await
|
||||
.action_by_contact(context, contact)
|
||||
@@ -793,7 +787,10 @@ pub(crate) async fn failed_sending_to(context: &Context, name: impl AsRef<str>)
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is disabled.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_disabled(context: &Context, by_contact: u32) -> String {
|
||||
pub(crate) async fn msg_ephemeral_timer_disabled(
|
||||
context: &Context,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
translated(context, StockMessage::MsgEphemeralTimerDisabled)
|
||||
.await
|
||||
.action_by_contact(context, by_contact)
|
||||
@@ -804,7 +801,7 @@ pub(crate) async fn msg_ephemeral_timer_disabled(context: &Context, by_contact:
|
||||
pub(crate) async fn msg_ephemeral_timer_enabled(
|
||||
context: &Context,
|
||||
timer: impl AsRef<str>,
|
||||
by_contact: u32,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
translated(context, StockMessage::MsgEphemeralTimerEnabled)
|
||||
.await
|
||||
@@ -814,7 +811,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to 1 minute.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: u32) -> String {
|
||||
pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: ContactId) -> String {
|
||||
translated(context, StockMessage::MsgEphemeralTimerMinute)
|
||||
.await
|
||||
.action_by_contact(context, by_contact)
|
||||
@@ -822,7 +819,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: u3
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to 1 hour.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: u32) -> String {
|
||||
pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
|
||||
translated(context, StockMessage::MsgEphemeralTimerHour)
|
||||
.await
|
||||
.action_by_contact(context, by_contact)
|
||||
@@ -830,7 +827,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: u32)
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to 1 day.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: u32) -> String {
|
||||
pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
|
||||
translated(context, StockMessage::MsgEphemeralTimerDay)
|
||||
.await
|
||||
.action_by_contact(context, by_contact)
|
||||
@@ -838,7 +835,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: u32)
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to 1 week.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: u32) -> String {
|
||||
pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
|
||||
translated(context, StockMessage::MsgEphemeralTimerWeek)
|
||||
.await
|
||||
.action_by_contact(context, by_contact)
|
||||
@@ -883,7 +880,7 @@ pub(crate) async fn error_no_network(context: &Context) -> String {
|
||||
}
|
||||
|
||||
/// Stock string: `Chat protection enabled.`.
|
||||
pub(crate) async fn protection_enabled(context: &Context, by_contact: u32) -> String {
|
||||
pub(crate) async fn protection_enabled(context: &Context, by_contact: ContactId) -> String {
|
||||
translated(context, StockMessage::ProtectionEnabled)
|
||||
.await
|
||||
.action_by_contact(context, by_contact)
|
||||
@@ -891,7 +888,7 @@ pub(crate) async fn protection_enabled(context: &Context, by_contact: u32) -> St
|
||||
}
|
||||
|
||||
/// Stock string: `Chat protection disabled.`.
|
||||
pub(crate) async fn protection_disabled(context: &Context, by_contact: u32) -> String {
|
||||
pub(crate) async fn protection_disabled(context: &Context, by_contact: ContactId) -> String {
|
||||
translated(context, StockMessage::ProtectionDisabled)
|
||||
.await
|
||||
.action_by_contact(context, by_contact)
|
||||
@@ -917,7 +914,7 @@ pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
|
||||
pub(crate) async fn msg_ephemeral_timer_minutes(
|
||||
context: &Context,
|
||||
minutes: impl AsRef<str>,
|
||||
by_contact: u32,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
translated(context, StockMessage::MsgEphemeralTimerMinutes)
|
||||
.await
|
||||
@@ -930,7 +927,7 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
|
||||
pub(crate) async fn msg_ephemeral_timer_hours(
|
||||
context: &Context,
|
||||
hours: impl AsRef<str>,
|
||||
by_contact: u32,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
translated(context, StockMessage::MsgEphemeralTimerHours)
|
||||
.await
|
||||
@@ -943,7 +940,7 @@ pub(crate) async fn msg_ephemeral_timer_hours(
|
||||
pub(crate) async fn msg_ephemeral_timer_days(
|
||||
context: &Context,
|
||||
days: impl AsRef<str>,
|
||||
by_contact: u32,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
translated(context, StockMessage::MsgEphemeralTimerDays)
|
||||
.await
|
||||
@@ -956,7 +953,7 @@ pub(crate) async fn msg_ephemeral_timer_days(
|
||||
pub(crate) async fn msg_ephemeral_timer_weeks(
|
||||
context: &Context,
|
||||
weeks: impl AsRef<str>,
|
||||
by_contact: u32,
|
||||
by_contact: ContactId,
|
||||
) -> String {
|
||||
translated(context, StockMessage::MsgEphemeralTimerWeeks)
|
||||
.await
|
||||
@@ -1113,7 +1110,7 @@ impl Context {
|
||||
pub(crate) async fn stock_protection_msg(
|
||||
&self,
|
||||
protect: ProtectionStatus,
|
||||
from_id: u32,
|
||||
from_id: ContactId,
|
||||
) -> String {
|
||||
match protect {
|
||||
ProtectionStatus::Unprotected => protection_enabled(self, from_id).await,
|
||||
@@ -1205,8 +1202,15 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_stock_string_repl_str() {
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
|
||||
// uses %1$s substitution
|
||||
assert_eq!(contact_verified(&t, "Foo").await, "Foo verified.");
|
||||
assert_eq!(
|
||||
contact_verified(&t, &contact).await,
|
||||
"Someone (someone@example.org) verified."
|
||||
);
|
||||
// We have no string using %1$d to test...
|
||||
}
|
||||
|
||||
@@ -1288,11 +1292,13 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 2);
|
||||
|
||||
let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0)).await.unwrap();
|
||||
let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let (self_talk_id, device_chat_id) = if chat0.is_self_talk() {
|
||||
(chats.get_chat_id(0), chats.get_chat_id(1))
|
||||
(chats.get_chat_id(0).unwrap(), chats.get_chat_id(1).unwrap())
|
||||
} else {
|
||||
(chats.get_chat_id(1), chats.get_chat_id(0))
|
||||
(chats.get_chat_id(1).unwrap(), chats.get_chat_id(0).unwrap())
|
||||
};
|
||||
|
||||
// delete self-talk first; this adds a message to device-chat about how self-talk can be restored
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! # Message summary for chatlist.
|
||||
|
||||
use crate::chat::Chat;
|
||||
use crate::constants::{Chattype, Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::constants::{Chattype, DC_CONTACT_ID_SELF};
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::dc_truncate;
|
||||
use crate::message::{Message, MessageState};
|
||||
use crate::message::{Message, MessageState, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::stock_str;
|
||||
@@ -137,7 +137,14 @@ impl Message {
|
||||
append_text = false;
|
||||
stock_str::videochat_invitation(context).await
|
||||
}
|
||||
_ => {
|
||||
Viewtype::Webxdc => {
|
||||
append_text = true;
|
||||
self.get_webxdc_info(context)
|
||||
.await
|
||||
.map(|info| info.name)
|
||||
.unwrap_or_else(|_| "ErrWebxdcName".to_string())
|
||||
}
|
||||
Viewtype::Text | Viewtype::Unknown => {
|
||||
if self.param.get_cmd() != SystemMessage::LocationOnly {
|
||||
"".to_string()
|
||||
} else {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::constants::{Blocked, DC_CONTACT_ID_SELF};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
|
||||
|
||||
@@ -5,13 +5,11 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
use std::panic;
|
||||
use std::str::FromStr;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ansi_term::Color;
|
||||
use async_std::channel::{self, Receiver, Sender};
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::{Arc, RwLock};
|
||||
use async_std::task;
|
||||
@@ -24,17 +22,15 @@ use crate::chat::{self, Chat, ChatId};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Chattype;
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF, DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::constants::{DC_CONTACT_ID_SELF, DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
|
||||
use crate::contact::{Contact, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::events::{Event, EventType};
|
||||
use crate::job::Action;
|
||||
use crate::key::{self, DcKey, KeyPair, KeyPairUse};
|
||||
use crate::message::{update_msg_state, Message, MessageState, MsgId};
|
||||
use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::param::{Param, Params};
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
@@ -43,6 +39,34 @@ pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avata
|
||||
static CONTEXT_NAMES: Lazy<std::sync::RwLock<BTreeMap<u32, String>>> =
|
||||
Lazy::new(|| std::sync::RwLock::new(BTreeMap::new()));
|
||||
|
||||
pub struct TestContextManager {
|
||||
log_tx: Sender<Event>,
|
||||
_log_sink: LogSink,
|
||||
}
|
||||
|
||||
impl TestContextManager {
|
||||
pub async fn new() -> Self {
|
||||
let (log_tx, _log_sink) = LogSink::create();
|
||||
Self { log_tx, _log_sink }
|
||||
}
|
||||
|
||||
pub async fn alice(&mut self) -> TestContext {
|
||||
TestContext::builder()
|
||||
.configure_alice()
|
||||
.with_log_sink(self.log_tx.clone())
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn bob(&mut self) -> TestContext {
|
||||
TestContext::builder()
|
||||
.configure_bob()
|
||||
.with_log_sink(self.log_tx.clone())
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TestContextBuilder {
|
||||
key_pair: Option<KeyPair>,
|
||||
@@ -275,27 +299,27 @@ impl TestContext {
|
||||
/// Panics if there is no message or on any error.
|
||||
pub async fn pop_sent_msg(&self) -> SentMessage {
|
||||
let start = Instant::now();
|
||||
let (rowid, foreign_id, raw_params) = loop {
|
||||
let (rowid, msg_id, payload, recipients) = loop {
|
||||
let row = self
|
||||
.ctx
|
||||
.sql
|
||||
.query_row(
|
||||
.query_row_optional(
|
||||
r#"
|
||||
SELECT id, foreign_id, param
|
||||
FROM jobs
|
||||
WHERE action=?
|
||||
ORDER BY desired_timestamp DESC;
|
||||
"#,
|
||||
paramsv![Action::SendMsgToSmtp],
|
||||
SELECT id, msg_id, mime, recipients
|
||||
FROM smtp
|
||||
ORDER BY id DESC"#,
|
||||
paramsv![],
|
||||
|row| {
|
||||
let id: u32 = row.get(0)?;
|
||||
let foreign_id: u32 = row.get(1)?;
|
||||
let param: String = row.get(2)?;
|
||||
Ok((id, foreign_id, param))
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let msg_id: MsgId = row.get(1)?;
|
||||
let mime: String = row.get(2)?;
|
||||
let recipients: String = row.get(3)?;
|
||||
Ok((rowid, msg_id, mime, recipients))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(row) = row {
|
||||
.await
|
||||
.expect("query_row_optional failed");
|
||||
if let Some(row) = row {
|
||||
break row;
|
||||
}
|
||||
if start.elapsed() < Duration::from_secs(3) {
|
||||
@@ -304,24 +328,18 @@ impl TestContext {
|
||||
panic!("no sent message found in jobs table");
|
||||
}
|
||||
};
|
||||
let id = MsgId::new(foreign_id);
|
||||
let params = Params::from_str(&raw_params).unwrap();
|
||||
let blob_path = params
|
||||
.get_blob(Param::File, &self.ctx, false)
|
||||
.await
|
||||
.expect("failed to parse blob from param")
|
||||
.expect("no Param::File found in Params")
|
||||
.to_abs_path();
|
||||
self.ctx
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
|
||||
.await
|
||||
.expect("failed to remove job");
|
||||
update_msg_state(&self.ctx, id, MessageState::OutDelivered).await;
|
||||
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
|
||||
.await
|
||||
.expect("failed to update message state");
|
||||
SentMessage {
|
||||
params,
|
||||
blob_path,
|
||||
sender_msg_id: id,
|
||||
payload,
|
||||
sender_msg_id: msg_id,
|
||||
recipients,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +363,7 @@ impl TestContext {
|
||||
let received_msg =
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n"
|
||||
.to_owned()
|
||||
+ &msg.payload();
|
||||
+ msg.payload();
|
||||
dc_receive_imf(&self.ctx, received_msg.as_bytes(), "INBOX", false)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -381,31 +399,56 @@ impl TestContext {
|
||||
.expect("failed to load msg")
|
||||
}
|
||||
|
||||
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
|
||||
pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact {
|
||||
let name = other
|
||||
.ctx
|
||||
.get_config(Config::Displayname)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
let addr = other
|
||||
.ctx
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
|
||||
// origin when creating this contact.
|
||||
let (contact_id, modified) =
|
||||
Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress)
|
||||
.await
|
||||
.unwrap();
|
||||
match modified {
|
||||
Modifier::None => (),
|
||||
Modifier::Modified => warn!(&self.ctx, "Contact {} modified by TestContext", &addr),
|
||||
Modifier::Created => warn!(&self.ctx, "Contact {} created by TestContext", &addr),
|
||||
}
|
||||
Contact::load_from_db(&self.ctx, contact_id).await.unwrap()
|
||||
}
|
||||
|
||||
/// Returns 1:1 [`Chat`] with another account, if it exists.
|
||||
///
|
||||
/// This first creates a contact using the configured details on the other account, then
|
||||
/// creates a 1:1 chat with this contact.
|
||||
pub async fn get_chat(&self, other: &TestContext) -> Option<Chat> {
|
||||
let contact = self.add_or_lookup_contact(other).await;
|
||||
match ChatId::lookup_by_contact(&self.ctx, contact.id)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
Some(id) => Some(Chat::load_from_db(&self.ctx, id).await.unwrap()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates or returns an existing 1:1 [`Chat`] with another account.
|
||||
///
|
||||
/// This first creates a contact using the configured details on the other account, then
|
||||
/// creates a 1:1 chat with this contact.
|
||||
pub async fn create_chat(&self, other: &TestContext) -> Chat {
|
||||
let (contact_id, _modified) = Contact::add_or_lookup(
|
||||
self,
|
||||
&other
|
||||
.ctx
|
||||
.get_config(Config::Displayname)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default(),
|
||||
&other
|
||||
.ctx
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat_id = ChatId::create_for_contact(self, contact_id).await.unwrap();
|
||||
let contact = self.add_or_lookup_contact(other).await;
|
||||
let chat_id = ChatId::create_for_contact(self, contact.id).await.unwrap();
|
||||
Chat::load_from_db(self, chat_id).await.unwrap()
|
||||
}
|
||||
|
||||
@@ -590,8 +633,8 @@ impl Drop for LogSink {
|
||||
/// passed through a SMTP-IMAP pipeline.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SentMessage {
|
||||
params: Params,
|
||||
blob_path: PathBuf,
|
||||
payload: String,
|
||||
recipients: String,
|
||||
pub sender_msg_id: MsgId,
|
||||
}
|
||||
|
||||
@@ -600,17 +643,17 @@ impl SentMessage {
|
||||
///
|
||||
/// If there are multiple recipients this is just a random one, so is not very useful.
|
||||
pub fn recipient(&self) -> EmailAddress {
|
||||
let raw = self
|
||||
.params
|
||||
.get(Param::Recipients)
|
||||
.expect("no recipients in params");
|
||||
let rcpt = raw.split(' ').next().expect("no recipient found");
|
||||
let rcpt = self
|
||||
.recipients
|
||||
.split(' ')
|
||||
.next()
|
||||
.expect("no recipient found");
|
||||
rcpt.parse().expect("failed to parse email address")
|
||||
}
|
||||
|
||||
/// The raw message payload.
|
||||
pub fn payload(&self) -> String {
|
||||
std::fs::read_to_string(&self.blob_path).unwrap()
|
||||
pub fn payload(&self) -> &str {
|
||||
&self.payload
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,7 +851,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
&contact_name,
|
||||
contact_id,
|
||||
msgtext.unwrap_or_default(),
|
||||
if msg.get_from_id() == 1u32 {
|
||||
if msg.get_from_id() == DC_CONTACT_ID_SELF {
|
||||
""
|
||||
} else if msg.get_state() == MessageState::InSeen {
|
||||
"[SEEN]"
|
||||
@@ -859,17 +902,10 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_with_both() {
|
||||
let (log_sender, _log_sink) = LogSink::create();
|
||||
let alice = TestContext::builder()
|
||||
.configure_alice()
|
||||
.with_log_sink(log_sender.clone())
|
||||
.build()
|
||||
.await;
|
||||
let bob = TestContext::builder()
|
||||
.configure_bob()
|
||||
.with_log_sink(log_sender)
|
||||
.build()
|
||||
.await;
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
alice.ctx.emit_event(EventType::Info("hello".into()));
|
||||
bob.ctx.emit_event(EventType::Info("there".into()));
|
||||
// panic!("Both fail");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! # Functions to update timestamps.
|
||||
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::contact::Contact;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::param::{Param, Params};
|
||||
use anyhow::Result;
|
||||
@@ -12,7 +12,7 @@ impl Context {
|
||||
/// (if we have a ContactId type at some point, the function should go there)
|
||||
pub(crate) async fn update_contacts_timestamp(
|
||||
&self,
|
||||
contact_id: u32,
|
||||
contact_id: ContactId,
|
||||
scope: Param,
|
||||
new_timestamp: i64,
|
||||
) -> Result<bool> {
|
||||
|
||||
1648
src/webxdc.rs
Normal file
1648
src/webxdc.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,9 +12,11 @@ Filename encoding | Encoded Words ([RFC 2047](https://tools.ietf.
|
||||
Identify server folders | IMAP LIST Extension ([RFC 6154](https://tools.ietf.org/html/rfc6154))
|
||||
Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177))
|
||||
Quota | IMAP QUOTA extension ([RFC 2087](https://tools.ietf.org/html/rfc2087))
|
||||
Seen status synchronization | IMAP CONDSTORE extension ([RFC 7162](https://tools.ietf.org/html/rfc7162))
|
||||
Authorization | OAuth2 ([RFC 6749](https://tools.ietf.org/html/rfc6749))
|
||||
End-to-end encryption | [Autocrypt Level 1](https://autocrypt.org/level1.html), OpenPGP ([RFC 4880](https://tools.ietf.org/html/rfc4880)), Security Multiparts for MIME ([RFC 1847](https://tools.ietf.org/html/rfc1847)) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html)
|
||||
Configuration assistance | [Autoconfigure](https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover](https://technet.microsoft.com/library/bb124251(v=exchg.150).aspx)
|
||||
Header encryption | [Protected Headers for Cryptographic E-mail](https://datatracker.ietf.org/doc/draft-autocrypt-lamps-protected-headers/)
|
||||
Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover](https://technet.microsoft.com/library/bb124251(v=exchg.150).aspx)
|
||||
Messenger functions | [Chat-over-Email](https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#chat-mail-specification)
|
||||
Detect mailing list | List-Id ([RFC 2919](https://tools.ietf.org/html/rfc2919)) and Precedence ([RFC 3834](https://tools.ietf.org/html/rfc3834))
|
||||
User and chat colors | [XEP-0392](https://xmpp.org/extensions/xep-0392.html): Consistent Color Generation
|
||||
|
||||
15
test-data/message/invalid_email_to.eml
Normal file
15
test-data/message/invalid_email_to.eml
Normal file
@@ -0,0 +1,15 @@
|
||||
Subject: Some subject
|
||||
Date: Sat, 01 Jan 2022 21:14:26 +0000
|
||||
Chat-Version: 1.0
|
||||
MIME-Version: 1.0
|
||||
Message-ID: <foo@example.org>
|
||||
To: <alice@example.org>, <bob@example.net>,
|
||||
<xxxxxxxx.xxxx@example.orgã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â£ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â£ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â°ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â£ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢âã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â£ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â¦ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã£â¢ã¢â£ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â¢ã£â£ã¢â£ã£â¢ã¢â£ã£â£ã¢â¢ã
|
||||
£â¢ã<C2A2>,
|
||||
<20>â¢ã£â£ã¢â£ã£â¢ã¢â¢ã£â£ã¢â¢ã£â¢ã¢â<C2A2><EFBFBD>@abcdef.example.net,
|
||||
<tmp.xxxxx@testrun.org>
|
||||
From: Claire <claire@example.org>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
X-Spam: Yes
|
||||
|
||||
Some message.
|
||||
@@ -1,43 +1,43 @@
|
||||
Return-Path: <anonymous@example.org>
|
||||
Return-Path: <bob@example.net>
|
||||
Delivered-To: anonymous@posteo.de
|
||||
Received: from proxy02.posteo.name ([127.0.0.1])
|
||||
by dovecot16.posteo.name (Dovecot) with LMTP id Cp2uFxP1sWHbCQEAchYRkQ
|
||||
for <anonymous@posteo.de>; Thu, 09 Dec 2021 13:25:38 +0100
|
||||
by dovecot16.posteo.name (Dovecot) with LMTP id GaxcARout2HxiwMAchYRkQ
|
||||
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100
|
||||
Received: from proxy02.posteo.de ([127.0.0.1])
|
||||
by proxy02.posteo.name (Dovecot) with LMTP id MWsaCwrvsWG0wgEAGFAyLg
|
||||
; Thu, 09 Dec 2021 13:25:38 +0100
|
||||
Received: from mailin06.posteo.de (unknown [10.0.1.6])
|
||||
by proxy02.posteo.de (Postfix) with ESMTPS id 4J8tXy0KkMz120l
|
||||
for <anonymous@posteo.de>; Thu, 9 Dec 2021 13:25:38 +0100 (CET)
|
||||
Received: from mx04.posteo.de (mailin06.posteo.de [127.0.0.1])
|
||||
by mailin06.posteo.de (Postfix) with ESMTPS id F24DE215B8
|
||||
for <anonymous@posteo.de>; Thu, 9 Dec 2021 13:25:37 +0100 (CET)
|
||||
by proxy02.posteo.name (Dovecot) with LMTP id q/LiCqwqt2FMTQEAGFAyLg
|
||||
; Mon, 13 Dec 2021 12:35:32 +0100
|
||||
Received: from mailin05.posteo.de (unknown [10.0.1.5])
|
||||
by proxy02.posteo.de (Postfix) with ESMTPS id 4JCKFJ1LCLz1214
|
||||
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100 (CET)
|
||||
Received: from mx03.posteo.de (mailin05.posteo.de [127.0.0.1])
|
||||
by mailin05.posteo.de (Postfix) with ESMTPS id 1B26420012
|
||||
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100 (CET)
|
||||
X-Virus-Scanned: amavisd-new at posteo.de
|
||||
X-Spam-Flag: NO
|
||||
X-Spam-Score: 0.011
|
||||
X-Spam-Level:
|
||||
X-Spam-Status: No, score=0.011 tagged_above=-1000 required=7
|
||||
tests=[HTML_MESSAGE=0.001, T_POSTEO_TLSINY=0.01] autolearn=disabled
|
||||
X-Posteo-Antispam-Signature: v=1; e=base64; a=aes-256-gcm; d=27yedFdXeAzOobR4x685XJ/5e6WQmX8PP5pSnOlGU2a9Ismhk38wb5AS44xh1yeL5PUxla78UEsHwGkPR0IyPRlHWaLMFLd5CJZN3GzFfrj/2CuB+cd1hOLpp9hRmCebc3rchuDr
|
||||
X-Posteo-Antispam-Signature: v=1; e=base64; a=aes-256-gcm; d=RzB41PpqvrD+cuxf3UAqQLiXQL4MHazHZcKeOYJw75deIl7zxtrLXqfAZCeq2IPKt/njRRONUbfuvNdvLxg4mBJ0Rnb53wFOKOqtEpTzxYoQff3yqBpGSohr0DBG26PyBHi7ba/7
|
||||
Authentication-Results: posteo.de; dmarc=none (p=none dis=none) header.from=example.org
|
||||
X-Posteo-TLS-Received-Status: TLSv1.2
|
||||
Received: from mail.example.org (mail.example.org [0.0.0.0])
|
||||
by mx04.posteo.de (Postfix) with ESMTPS id 4J8tXx38vRz10yw
|
||||
for <anonymous@posteo.at>; Thu, 9 Dec 2021 13:25:37 +0100 (CET)
|
||||
Received: from [192.168.1.11] (port=22105 helo=mail.example.org)
|
||||
by mx03.posteo.de (Postfix) with ESMTPS id 4JCKFH2ZM7zyx0
|
||||
for <alice@example.org>; Mon, 13 Dec 2021 12:35:31 +0100 (CET)
|
||||
Received: from [192.168.1.11] (port=27040 helo=mail.example.org)
|
||||
by mail.example.org with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
|
||||
(Exim 4.94.2)
|
||||
(envelope-from <anonymous@example.org>)
|
||||
id 1mvIUG-0007VC-2U
|
||||
for anonymous@posteo.at; Thu, 09 Dec 2021 13:25:24 +0100
|
||||
From: Anonymous <anonymous@example.org>
|
||||
To: Anonymous <anonymous@posteo.at>
|
||||
(envelope-from <bob@example.net>)
|
||||
id 1mwjc6-0003hM-2K
|
||||
for alice@example.org; Mon, 13 Dec 2021 12:35:26 +0100
|
||||
From: Anonymous_2 <bob@example.net>
|
||||
To: Anonymous_1 <alice@example.org>
|
||||
Subject: Gelesen: Test message
|
||||
Thread-Topic: Test message
|
||||
Thread-Index: AQHX7Dt/+5f88Aokk0KrqG0hbF8dN6wqFvxh
|
||||
Date: Thu, 9 Dec 2021 12:25:24 +0000
|
||||
Message-ID: <1711fc3548cd4b2699ccd4fffac17713@anonymous>
|
||||
In-Reply-To: <75dd051097b02468183707ad0dd62ebd@posteo.de>
|
||||
Thread-Index: AQHX8BVZ9B3+kB6CNUCtJ9eQIONSNawwSqpi
|
||||
Date: Mon, 13 Dec 2021 11:35:26 +0000
|
||||
Message-ID: <59b1d0c94a8d4834b7ab779a76647d44@mail.example.org>
|
||||
In-Reply-To: <d5904dc344eeb5deaf9bb44603f0c716@posteo.de>
|
||||
Accept-Language: de-AT, de-DE, en-US
|
||||
Content-Language: de-DE
|
||||
X-MS-Has-Attach:
|
||||
@@ -45,29 +45,29 @@ X-MS-TNEF-Correlator:
|
||||
x-ms-exchange-transport-fromentityheader: Hosted
|
||||
x-originating-ip: [192.168.120.215]
|
||||
Content-Type: multipart/report;
|
||||
boundary="_000_1711fc3548cd4b2699ccd4fffac17713anonymous_";
|
||||
boundary="_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_";
|
||||
report-type=disposition-notification
|
||||
MIME-Version: 1.0
|
||||
|
||||
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_
|
||||
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="_002_1711fc3548cd4b2699ccd4fffac17713anonymous_"
|
||||
boundary="_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_"
|
||||
|
||||
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_
|
||||
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
|
||||
Content-Type: text/plain; charset="iso-8859-1"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Ihre Nachricht
|
||||
|
||||
An: Anonymous
|
||||
An: Anonymous_2
|
||||
Betreff: Test message
|
||||
Gesendet: Mittwoch, 8. Dezember 2021 14:57:05 (UTC+01:00) Amsterdam, Ber=
|
||||
lin, Bern, Rom, Stockholm, Wien
|
||||
Gesendet: Montag, 13. Dezember 2021 12:33:58 (UTC+01:00) Amsterdam, Berl=
|
||||
in, Bern, Rom, Stockholm, Wien
|
||||
|
||||
wurde am Donnerstag, 9. Dezember 2021 13:24:34 (UTC+01:00) Amsterdam, Berl=
|
||||
in, Bern, Rom, Stockholm, Wien gelesen.
|
||||
wurde am Montag, 13. Dezember 2021 12:34:40 (UTC+01:00) Amsterdam, Berlin,=
|
||||
Bern, Rom, Stockholm, Wien gelesen.
|
||||
|
||||
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_
|
||||
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
|
||||
Content-Type: text/html; charset="iso-8859-1"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
@@ -83,26 +83,26 @@ ding-left: 4pt; border-left: #800000 2px solid; } --></style>
|
||||
<font size=3D"2"><span style=3D"font-size:10pt;">
|
||||
<div class=3D"PlainText">Ihre Nachricht <br>
|
||||
<br>
|
||||
An: Anonymous<br>
|
||||
An: Anonymous_2<br>
|
||||
Betreff: Test message<br>
|
||||
Gesendet: Mittwoch, 8. Dezember 2021 14:57:05 (UTC+01:00) =
|
||||
Amsterdam, Berlin, Bern, Rom, Stockholm, Wien<br>
|
||||
Gesendet: Montag, 13. Dezember 2021 12:33:58 (UTC+01:00) A=
|
||||
msterdam, Berlin, Bern, Rom, Stockholm, Wien<br>
|
||||
<br>
|
||||
wurde am Donnerstag, 9. Dezember 2021 13:24:34 (UTC+01:00) Amster=
|
||||
dam, Berlin, Bern, Rom, Stockholm, Wien gelesen.</div>
|
||||
wurde am Montag, 13. Dezember 2021 12:34:40 (UTC+01:00) Amsterdam=
|
||||
, Berlin, Bern, Rom, Stockholm, Wien gelesen.</div>
|
||||
</span></font>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_--
|
||||
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_--
|
||||
|
||||
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_
|
||||
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
|
||||
Content-Type: message/disposition-notification
|
||||
|
||||
Final-recipient: RFC822; anonymous@example.org
|
||||
Final-recipient: RFC822; bob@example.net
|
||||
Disposition: automatic-action/MDN-sent-automatically; displayed
|
||||
X-MSExch-Correlation-Key: coNC5vaCQkiAOjek1v1Uew==
|
||||
X-Display-Name: Anonymous
|
||||
X-MSExch-Correlation-Key: nf7/jgN6Qk+WzsrkY5s9WA==
|
||||
X-Display-Name: Anonymous_2
|
||||
|
||||
|
||||
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_--
|
||||
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_--
|
||||
|
||||
34
test-data/message/ms_exchange_report_original_message.eml
Normal file
34
test-data/message/ms_exchange_report_original_message.eml
Normal file
@@ -0,0 +1,34 @@
|
||||
Received: We have to put a Received header here. Otherwise, the message would be ignored
|
||||
because DC thinks it's a draft, and the test fails.
|
||||
Alternatively, we could configure the Sentobox folder in the test.
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="=_3293d145f71bf71c4bb97415536759d6"
|
||||
Date: Mon, 13 Dec 2021 12:33:58 +0100
|
||||
From: Anonymous_1 <alice@example.org>
|
||||
To: Anonymous_2 <bob@example.net>
|
||||
Subject: Test message
|
||||
Return-Receipt-To: Anonymous_1 <alice@example.org>
|
||||
Disposition-Notification-To: Anonymous_1 <alice@example.org>
|
||||
Message-ID: <d5904dc344eeb5deaf9bb44603f0c716@posteo.de>
|
||||
X-Sender: alice@example.org
|
||||
User-Agent: Posteo Webmail
|
||||
|
||||
--=_3293d145f71bf71c4bb97415536759d6
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Type: text/plain; charset=US-ASCII
|
||||
|
||||
This is a test!
|
||||
|
||||
Best regards
|
||||
--=_3293d145f71bf71c4bb97415536759d6
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">
|
||||
<html><body style=3D'font-size: 10pt; font-family: Verdana,Geneva,sans-seri=
|
||||
f'>
|
||||
This is a test!<br /><br />Best regards
|
||||
</body></html>
|
||||
|
||||
--=_3293d145f71bf71c4bb97415536759d6--
|
||||
21
test-data/message/webxdc_bad_extension.eml
Normal file
21
test-data/message/webxdc_bad_extension.eml
Normal file
@@ -0,0 +1,21 @@
|
||||
Subject: webxdc object attached
|
||||
Message-ID: 67890@example.org
|
||||
Date: Fri, 03 Dec 2021 10:00:27 +0000
|
||||
To: alice@example.org
|
||||
From: bob@example.org
|
||||
Chat-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary="==BREAK=="
|
||||
|
||||
|
||||
--==BREAK==
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
webxdc with bad extension and bad content.
|
||||
|
||||
--==BREAK==
|
||||
Content-Type: application/webxdc+zip
|
||||
Content-Disposition: attachment; filename=index.html
|
||||
|
||||
<html>hey!<html>
|
||||
|
||||
--==BREAK==--
|
||||
29
test-data/message/webxdc_good_extension.eml
Normal file
29
test-data/message/webxdc_good_extension.eml
Normal file
@@ -0,0 +1,29 @@
|
||||
Subject: webxdc object attached
|
||||
Message-ID: 12345@example.org
|
||||
Date: Fri, 03 Dec 2021 10:00:27 +0000
|
||||
To: alice@example.org
|
||||
From: bob@example.org
|
||||
Chat-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary="==BREAK=="
|
||||
|
||||
|
||||
--==BREAK==
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
webxdc with good extension;
|
||||
the mimetype is ignored then,
|
||||
content is checked.
|
||||
|
||||
--==BREAK==
|
||||
Content-Type: text/html
|
||||
Content-Disposition: attachment; filename=minimal.xdc
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
UEsDBBQAAgAIAFJqnVOItjSofAAAALwAAAAKABwAaW5kZXguaHRtbFVUCQADG1LMYV1SzGF1eAsAAQ
|
||||
T1AQAABBQAAACzUXTxdw6JDHBVyCjJzbHjsoFQCgo2SfkplSCGgkJJanGJIphlU5xclFlQAhFWUEjJ
|
||||
Ty7NTc0r0SsvyixJ1VAqyU8EqlTStIYo1kdWbZOXj6o5uiQjsxhodkWJQnFGfmlOikJefolCUiqIV5
|
||||
4XCzUCWZeNPsRNNvoQRwIAUEsBAh4DFAACAAgAUmqdU4i2NKh8AAAAvAAAAAoAGAAAAAAAAQAAAKSB
|
||||
AAAAAGluZGV4Lmh0bWxVVAUAAxtSzGF1eAsAAQT1AQAABBQAAABQSwUGAAAAAAEAAQBQAAAAwAAAAA
|
||||
AA
|
||||
|
||||
--==BREAK==--
|
||||
BIN
test-data/webxdc/invalid-no-zip-but-7z.xdc
Normal file
BIN
test-data/webxdc/invalid-no-zip-but-7z.xdc
Normal file
Binary file not shown.
BIN
test-data/webxdc/minimal.xdc
Normal file
BIN
test-data/webxdc/minimal.xdc
Normal file
Binary file not shown.
BIN
test-data/webxdc/no-index-html.xdc
Normal file
BIN
test-data/webxdc/no-index-html.xdc
Normal file
Binary file not shown.
BIN
test-data/webxdc/some-files.xdc
Normal file
BIN
test-data/webxdc/some-files.xdc
Normal file
Binary file not shown.
BIN
test-data/webxdc/with-jpg-icon.xdc
Normal file
BIN
test-data/webxdc/with-jpg-icon.xdc
Normal file
Binary file not shown.
BIN
test-data/webxdc/with-manifest-and-png-icon.xdc
Normal file
BIN
test-data/webxdc/with-manifest-and-png-icon.xdc
Normal file
Binary file not shown.
BIN
test-data/webxdc/with-manifest-empty-name.xdc
Normal file
BIN
test-data/webxdc/with-manifest-empty-name.xdc
Normal file
Binary file not shown.
BIN
test-data/webxdc/with-manifest-no-name.xdc
Normal file
BIN
test-data/webxdc/with-manifest-no-name.xdc
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user