mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
274 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
f3a716fac6 | ||
|
|
72659580de | ||
|
|
30cb0cbcfd | ||
|
|
4136217249 | ||
|
|
246cae5d9e | ||
|
|
12313543ca | ||
|
|
87e3dead14 | ||
|
|
9af36460c2 | ||
|
|
2e2d881e01 | ||
|
|
db58946312 | ||
|
|
9a02a58273 | ||
|
|
147f5c1e0d | ||
|
|
f0ca50ba27 | ||
|
|
83137b5968 | ||
|
|
12823c2213 | ||
|
|
0b810d7d65 | ||
|
|
7aebdc9b7b | ||
|
|
6859b651a8 | ||
|
|
d47680733b | ||
|
|
273a38d781 | ||
|
|
93d1162caf | ||
|
|
8d550a66a3 | ||
|
|
4c58e05be3 | ||
|
|
500563054e | ||
|
|
2a11f8f59d | ||
|
|
9e7bdc579e | ||
|
|
61af0c9ac4 | ||
|
|
91f02ad553 | ||
|
|
b20d3dfc10 | ||
|
|
3e7666021c | ||
|
|
ae58bceeb9 | ||
|
|
6665b9e13c | ||
|
|
367a9705e9 | ||
|
|
8d3a1e84c3 | ||
|
|
d009835210 | ||
|
|
83a664ca68 | ||
|
|
d98d1857a4 | ||
|
|
33a514aa54 | ||
|
|
0aefdc85e6 | ||
|
|
3ff0964f02 | ||
|
|
cf33db3dcb | ||
|
|
dae80cbe35 | ||
|
|
f8b4ef26b3 | ||
|
|
db991453b0 | ||
|
|
c11ce4c8d4 | ||
|
|
92e300cb9f | ||
|
|
d210e0bffe | ||
|
|
4d4968f358 | ||
|
|
8dfede148a | ||
|
|
6d125028f5 | ||
|
|
7ff3cf4af0 | ||
|
|
bb3353397d | ||
|
|
572260ec29 | ||
|
|
0b1faa0523 | ||
|
|
3b6c3e10d7 | ||
|
|
d3909a5483 | ||
|
|
645fd10446 | ||
|
|
ee7e29fb3a | ||
|
|
bdc3a4d24c | ||
|
|
88ccda139e | ||
|
|
63e78bae37 | ||
|
|
b50f211c28 | ||
|
|
5efbd9c7f5 | ||
|
|
7f1d2ea11f | ||
|
|
46eb391a1b | ||
|
|
b166cc5bf4 | ||
|
|
1d0f6aad95 | ||
|
|
764aa71770 | ||
|
|
21e9206a77 | ||
|
|
5f29977c50 | ||
|
|
ee4c9bc01e | ||
|
|
4a8259cb12 | ||
|
|
a6441d70f0 | ||
|
|
01db8d0130 | ||
|
|
8ad9db5572 | ||
|
|
607cd23014 | ||
|
|
220758d244 | ||
|
|
7ab71bb468 | ||
|
|
a74377b620 | ||
|
|
c9effa3c06 | ||
|
|
8b8102334b | ||
|
|
5dedb86472 | ||
|
|
d93f77f991 | ||
|
|
5409bc575e | ||
|
|
576d31206d | ||
|
|
86e3297414 | ||
|
|
ab4a947456 | ||
|
|
5ce2581b4c | ||
|
|
c0d6c6b882 | ||
|
|
2bc8c967b1 | ||
|
|
fee08f0eeb | ||
|
|
078c3d05d7 | ||
|
|
7f97768c56 | ||
|
|
ecd548a7aa | ||
|
|
62efb0795b | ||
|
|
53f042ee08 | ||
|
|
6ce97bd0cd | ||
|
|
c29149e74c | ||
|
|
487f7593ce | ||
|
|
6b3b33d2a0 | ||
|
|
2d70ccc2bf | ||
|
|
e90fc9504a | ||
|
|
5108314c03 | ||
|
|
c1e144d4db | ||
|
|
f2b86a1c0f | ||
|
|
9583d41446 | ||
|
|
e594064f93 | ||
|
|
7c52fd95ec | ||
|
|
d0d5ac6e87 | ||
|
|
416bf3a829 | ||
|
|
cc78347293 | ||
|
|
27e4ae992a | ||
|
|
a1767dc153 | ||
|
|
eb610c27bf | ||
|
|
016fb2ceb2 | ||
|
|
5c571520a0 | ||
|
|
ddefd2cf09 | ||
|
|
30a3eeece8 | ||
|
|
5919388588 | ||
|
|
2cc738f481 | ||
|
|
babd405928 | ||
|
|
e885857875 | ||
|
|
a1d57a2645 | ||
|
|
a28aecd4d1 | ||
|
|
a3f1ff2827 | ||
|
|
c4d1a639b0 | ||
|
|
8732b7a55c | ||
|
|
1b9148f28e | ||
|
|
e0129c3b43 | ||
|
|
60d41022ea | ||
|
|
dd4f2ac671 | ||
|
|
eebb2a3b68 | ||
|
|
0d62069b67 | ||
|
|
56cf2e6596 | ||
|
|
59bd5481b9 | ||
|
|
6c8da526a0 | ||
|
|
13bc8b78d7 | ||
|
|
c7c68094d9 |
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."
|
||||
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@@ -8,6 +8,9 @@ on:
|
||||
- staging
|
||||
- trying
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
|
||||
jobs:
|
||||
|
||||
fmt:
|
||||
@@ -21,6 +24,8 @@ jobs:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
@@ -35,6 +40,8 @@ jobs:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -94,31 +101,14 @@ jobs:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-index-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: target
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
|
||||
- name: check
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: check
|
||||
args: --all --bins --examples --tests --features repl
|
||||
args: --all --bins --examples --tests --features repl --benches
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
@@ -150,4 +140,4 @@ jobs:
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e lint,doc,py3
|
||||
run: tox -e lint,mypy,doc,py3
|
||||
|
||||
146
CHANGELOG.md
146
CHANGELOG.md
@@ -1,5 +1,147 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
## 1.70.0
|
||||
|
||||
### Fixes
|
||||
- fix: do not abort Param parsing on unknown keys #2856
|
||||
- fix: execute `Chat-Group-Member-Removed:` even when arriving disordered #2857
|
||||
|
||||
|
||||
## 1.69.0
|
||||
|
||||
### Fixes
|
||||
- fix group-related system messages in multi-device setups #2848
|
||||
- fix "Google Workspace" (former "G Suite") issues related to bad resolvers #2852
|
||||
|
||||
|
||||
## 1.68.0
|
||||
|
||||
### Fixes
|
||||
- fix chat assignment when forwarding #2843
|
||||
- fix layout issues with the generated QR code svg #2842
|
||||
|
||||
|
||||
## 1.67.0
|
||||
|
||||
### API changes
|
||||
- `dc_get_securejoin_qr_svg(chat_id)` added #2815
|
||||
- added stock-strings `DC_STR_SETUP_CONTACT_QR_DESC` and `DC_STR_SECURE_JOIN_GROUP_QR_DESC`
|
||||
|
||||
|
||||
## 1.66.0
|
||||
|
||||
### API changes
|
||||
- `dc_contact_get_last_seen()` added #2823
|
||||
- python: `Contact.last_seen` added #2823
|
||||
- removed `DC_STR_NEWGROUPDRAFT`, we don't set draft after creating group anymore #2805
|
||||
|
||||
### Changes
|
||||
- python: add cutil.from_optional_dc_charpointer() #2824
|
||||
- refactorings #2807 #2822 #2825
|
||||
|
||||
|
||||
## 1.65.0
|
||||
|
||||
### Changes
|
||||
- python: add mypy support and some type hints #2809
|
||||
|
||||
### Fixes
|
||||
- do not disable ephemeral timer when downloading a message partially #2811
|
||||
- apply existing ephemeral timer also to partially downloaded messages;
|
||||
after full download, the ephemeral timer starts over #2811
|
||||
- replace user-visible error on verification failure with warning;
|
||||
the error is logged to the corresponding chat anyway #2808
|
||||
|
||||
|
||||
## 1.64.0
|
||||
|
||||
### Fixes
|
||||
@@ -50,10 +192,10 @@
|
||||
## 1.61.0
|
||||
|
||||
### API Changes
|
||||
- download-on-demand added: `dc_msg_get_download_status()`, `dc_download_full_msg()`
|
||||
- download-on-demand added: `dc_msg_get_download_state()`, `dc_download_full_msg()`
|
||||
and `download_limit` config option #2631 #2696
|
||||
- `dc_create_broadcast_list()` and chat type `DC_CHAT_TYPE_BROADCAST` added #2707 #2722
|
||||
- allow ui-specific configs: `dc_set_ui_config()` and `dc_get_ui_config()` #2672
|
||||
- allow ui-specific configs using `ui.`-prefix in key (`dc_set_config(context, "ui.*", value)`) #2672
|
||||
- new strings from `DC_STR_PARTIAL_DOWNLOAD_MSG_BODY`
|
||||
to `DC_STR_PART_OF_TOTAL_USED` #2631 #2694 #2707 #2723
|
||||
- emit warnings and errors from account manager with account-id 0 #2712
|
||||
|
||||
860
Cargo.lock
generated
860
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
37
Cargo.toml
37
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.64.0"
|
||||
version = "1.74.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -12,6 +12,9 @@ debug = 0
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
[patch.crates-io]
|
||||
rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch="master" }
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
@@ -22,12 +25,11 @@ 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-tar = "0.4"
|
||||
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" }
|
||||
@@ -36,8 +38,6 @@ 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"] }
|
||||
indexmap = "1.7"
|
||||
itertools = "0.10"
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
@@ -47,27 +47,27 @@ native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.8.0"
|
||||
once_cell = "1.9.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.18"
|
||||
r2d2_sqlite = "0.19"
|
||||
rand = "0.7"
|
||||
regex = "1.5"
|
||||
rusqlite = "0.25"
|
||||
rusqlite = { version = "0.26", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "9.0", optional = true }
|
||||
rustyline = { version = "9", optional = true }
|
||||
sanitize-filename = "0.3"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.9"
|
||||
sha2 = "0.9"
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1"
|
||||
stop-token = "0.6"
|
||||
strum = "0.22"
|
||||
strum_macros = "0.22"
|
||||
stop-token = "0.7"
|
||||
strum = "0.23"
|
||||
strum_macros = "0.23"
|
||||
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
@@ -75,6 +75,10 @@ url = "2"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
fast-socks5 = "0.4"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "4.2.1"
|
||||
textwrap = "0.14.2"
|
||||
zip = { version = "0.5.13", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
@@ -82,9 +86,8 @@ async-std = { version = "1", features = ["unstable", "attributes"] }
|
||||
criterion = "0.3"
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_assertions = "1.0"
|
||||
pretty_env_logger = "0.4"
|
||||
proptest = "1"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
|
||||
[workspace]
|
||||
@@ -120,5 +123,5 @@ harness = false
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled-sqlcipher-vendored-openssl"]
|
||||
nightly = ["pgp/nightly"]
|
||||
|
||||
10
README.md
10
README.md
@@ -125,11 +125,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 |
10
assets/qrcode_logo_footer.svg
Normal file
10
assets/qrcode_logo_footer.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-weight:bold;font-size:24.4118px;line-height:1.25;font-family:sans-serif;fill:#aaaaaa;fill-opacity:1;stroke:none;stroke-width:0.915439"
|
||||
x="42.325161"
|
||||
y="23.32255"
|
||||
id="text72398">get.delta.chat</text>
|
||||
<path
|
||||
id="path84310"
|
||||
style="opacity:0.25;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.915439"
|
||||
d="M 17.13769,0.00129321 C 7.6753075,0.11650893 0,7.8915283 0,17.362467 c 0,9.47094 7.6753075,17.059745 17.13769,16.944599 8.99669,-0.03598 6.880074,-5.025654 16.824785,-0.405885 -5.447648,-8.510047 0.184241,-9.642482 0.311117,-16.955289 0,-9.4709395 -7.673512,-17.0597453 -17.135895,-16.94459879 z M 17.0769,4.9986797 c 1.84214,0 3.447355,0.253959 4.815003,0.7616693 1.381603,0.5076411 2.072253,1.207862 2.072253,2.0990711 0,0.4286855 -0.167495,0.7836052 -0.50242,1.0656242 -0.334921,0.2819844 -0.724544,0.4237724 -1.171121,0.4237724 -0.641952,0 -1.396532,-0.3909376 -2.261778,-1.169353 C 19.14963,7.3898036 18.402555,6.83791 17.788507,6.5220182 17.188416,6.1950547 16.484552,6.0321266 15.675129,6.0321266 c -1.032717,0 -1.883352,0.1854523 -2.553215,0.5578447 -0.655913,0.372254 -0.98517,0.8460916 -0.98517,1.4214436 0,0.5414792 0.272815,1.0495355 0.817093,1.5233385 0.544275,0.4738026 1.946291,1.3367446 4.207097,2.5889976 2.414319,1.342419 4.117377,2.390985 5.108232,3.146807 1.004795,0.755857 1.821505,1.675853 2.449514,2.758846 0.628002,1.082993 0.942253,2.227607 0.942253,3.434674 0,2.120834 -0.929555,3.993314 -2.785656,5.617786 -1.84214,1.613228 -3.99694,2.41915 -6.467082,2.41915 -2.246845,0 -4.145607,-0.647976 -5.694677,-1.945312 -1.5490699,-1.297336 -2.3225722,-3.028063 -2.3225722,-5.194049 0,-2.087031 0.8506345,-3.83094 2.5532182,-5.229825 1.716541,-1.398884 3.824203,-2.245599 6.322256,-2.538897 -0.697774,-0.631749 -1.668763,-1.387225 -2.910816,-2.267155 -1.367648,-0.970199 -2.287914,-1.73045 -2.762402,-2.283243 -0.474491,-0.5640381 -0.711618,-1.1795944 -0.711618,-1.8451814 0,-0.9927581 0.572093,-1.7710351 1.716451,-2.3351077 1.144362,-0.5753173 2.636724,-0.8635642 4.478865,-0.8635642 z m 1.110327,10.3738083 c -4.005262,0.5302 -6.007576,2.75279 -6.007576,6.667322 0,2.01932 0.49495,3.587291 1.485805,4.704157 1.004806,1.116832 2.169696,1.675299 3.495479,1.675299 1.381602,0 2.520072,-0.535632 3.413229,-1.60738 0.893168,-1.082959 1.339187,-2.545264 1.339187,-4.384079 0,-2.662348 -1.242022,-5.013441 -3.726124,-7.055319 z" />
|
||||
@@ -8,9 +8,7 @@ async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = 100;
|
||||
let context = Context::new("FakeOS".into(), dbfile.into(), id)
|
||||
.await
|
||||
.unwrap();
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
|
||||
let book = (0..n)
|
||||
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
|
||||
|
||||
@@ -8,7 +8,7 @@ async fn create_accounts(n: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 2..n {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
|
||||
@@ -6,9 +6,7 @@ use std::path::Path;
|
||||
async fn search_benchmark(path: impl AsRef<Path>) {
|
||||
let dbfile = path.as_ref();
|
||||
let id = 100;
|
||||
let context = Context::new("FakeOS".into(), dbfile.into(), id)
|
||||
.await
|
||||
.unwrap();
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
|
||||
for _ in 0..10u32 {
|
||||
context.search_msgs(None, "hello").await.unwrap();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.64.0"
|
||||
version = "1.74.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,
|
||||
|
||||
@@ -179,24 +179,66 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
|
||||
// create/open/config/information
|
||||
|
||||
/**
|
||||
* Create a new context object. After creation it is usually
|
||||
* opened, connected and mails are fetched.
|
||||
* Create a new context object and try to open it without passphrase. If
|
||||
* database is encrypted, the result is the same as using
|
||||
* dc_context_new_closed() and the database should be opened with
|
||||
* dc_context_open() before using.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param os_name is only for decorative use.
|
||||
* You can give the name of the app, the operating system,
|
||||
* the used environment and/or the version here.
|
||||
* @param os_name Deprecated, pass NULL or empty string here.
|
||||
* @param dbfile The file to use to store the database,
|
||||
* something like `~/file` won't work, use absolute paths.
|
||||
* @param blobdir Deprecated, pass NULL or an empty string here.
|
||||
* @return A context object with some public members.
|
||||
* The object must be passed to the other context functions
|
||||
* and must be freed using dc_context_unref() after usage.
|
||||
*/
|
||||
dc_context_t* dc_context_new (const char* os_name, const char* dbfile, const char* blobdir);
|
||||
|
||||
|
||||
/**
|
||||
* Create a new context object. After creation it is usually opened with
|
||||
* dc_context_open() and started with dc_start_io() so it is connected and
|
||||
* mails are fetched.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param dbfile The file to use to store the database,
|
||||
* something like `~/file` won't work, use absolute paths.
|
||||
* @return A context object with some public members.
|
||||
* The object must be passed to the other context functions
|
||||
* and must be freed using dc_context_unref() after usage.
|
||||
*
|
||||
* If you want to use multiple context objects at the same time,
|
||||
* this can be managed using dc_accounts_t.
|
||||
*/
|
||||
dc_context_t* dc_context_new (const char* os_name, const char* dbfile, const char* blobdir);
|
||||
dc_context_t* dc_context_new_closed (const char* dbfile);
|
||||
|
||||
|
||||
/**
|
||||
* Opens the database with the given passphrase. This can only be used on
|
||||
* closed context, such as created by dc_context_new_closed(). If the database
|
||||
* is new, this operation sets the database passphrase. For existing databases
|
||||
* the passphrase should be the one used to encrypt the database the first
|
||||
* time.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param passphrase The passphrase to use with the database. Pass NULL or
|
||||
* empty string to use no passphrase and no encryption.
|
||||
* @return 1 if the database is opened with this passphrase, 0 if the
|
||||
* passphrase is incorrect and on error.
|
||||
*/
|
||||
int dc_context_open (dc_context_t *context, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Returns 1 if database is open.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @return 1 if database is open, 0 if database is closed
|
||||
*/
|
||||
int dc_context_is_open (dc_context_t *context);
|
||||
|
||||
|
||||
/**
|
||||
@@ -279,7 +321,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
|
||||
* - `selfstatus` = Own status to display e.g. in email footers, defaults to a standard text defined by #DC_STR_STATUSLINE
|
||||
* - `selfstatus` = Own status to display e.g. in email footers, defaults to empty
|
||||
* - `selfavatar` = File containing avatar. Will immediately be copied to the
|
||||
* `blobdir`; the original image will not be needed anymore.
|
||||
* NULL to remove the avatar.
|
||||
@@ -293,18 +335,20 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 1=send a copy of outgoing messages to self.
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||
* - `inbox_watch` = 1=watch `INBOX`-folder for changes (default),
|
||||
* 0=do not watch the `INBOX`-folder,
|
||||
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
|
||||
* 0=do not watch the `Sent`-folder (default),
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `sentbox_watch`= 1=watch `Sent`-folder for changes (default),
|
||||
* 0=do not watch the `Sent`-folder,
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `mvbox_watch` = 1=watch `DeltaChat`-folder for changes (default),
|
||||
* 0=do not watch the `DeltaChat`-folder,
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `mvbox_move` = 1=heuristically detect chat-messages
|
||||
* and move them to the `DeltaChat`-folder,
|
||||
* - `mvbox_move` = 1=detect chat messages,
|
||||
* move them to the `DeltaChat` folder,
|
||||
* and watch the `DeltaChat` folder for updates (default),
|
||||
* 0=do not move chat-messages
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
|
||||
* `DeltaChat` folder. Messages will still be fetched from the
|
||||
* spam folder and `sendbox_watch` will also still be respected
|
||||
* if enabled.
|
||||
* 0=watch all folders normally (default)
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||
* show direct replies to chats only (default),
|
||||
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
||||
@@ -683,8 +727,8 @@ void dc_maybe_network (dc_context_t* context);
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param addr The email address of the user. This must match the
|
||||
* configured_addr setting of the context as well as the UID of the key.
|
||||
* @param public_data The public key as base64.
|
||||
* @param secret_data The secret key as base64.
|
||||
* @param public_data ASCII armored public key.
|
||||
* @param secret_data ASCII armored secret key.
|
||||
* @return 1 on success, 0 on failure.
|
||||
*/
|
||||
int dc_preconfigure_keypair (dc_context_t* context, const char *addr, const char *public_data, const char *secret_data);
|
||||
@@ -944,6 +988,64 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* An webxdc instance send a status update to its other members.
|
||||
*
|
||||
* In js-land, that would be mapped to sth. as:
|
||||
* ```
|
||||
* success = window.webxdc.sendUpdate('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4');
|
||||
* ```
|
||||
* `context` and `msg_id` is not needed in js as that is unique within an webxdc instance.
|
||||
* See dc_get_webxdc_status_updates() for the receiving counterpart.
|
||||
*
|
||||
* If the webxdc instance is a draft, the update is not send immediately.
|
||||
* Instead, the updates are collected and sent out in batch when the instance is actually sent.
|
||||
* This allows preparing webxdc instances,
|
||||
* eg. defining a poll with predefined answers.
|
||||
*
|
||||
* Other members will be informed by #DC_EVENT_WEBXDC_STATUS_UPDATE that there is a new update.
|
||||
* You will also get the #DC_EVENT_WEBXDC_STATUS_UPDATE yourself
|
||||
* and the update you're sent will also be included in dc_get_webxdc_status_updates().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object
|
||||
* @param msg_id id of the message with the webxdc instance
|
||||
* @param json program-readable data, the actual payload
|
||||
* @param descr user-visible description of the json-data,
|
||||
* in case of a chess game, eg. the move.
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const char* json, const char* descr);
|
||||
|
||||
|
||||
/**
|
||||
* Get webxdc status updates.
|
||||
* The status updates may be sent by yourself or by other members using dc_send_webxdc_status_update().
|
||||
* In both cases, you will be informed by #DC_EVENT_WEBXDC_STATUS_UPDATE
|
||||
* whenever there is a new update.
|
||||
*
|
||||
* In js-land, that would be mapped to sth. as:
|
||||
* ```
|
||||
* window.webxdc.setUpdateListener((update) => {
|
||||
* if (update.payload.action === "move") {
|
||||
* print(update.payload.src)
|
||||
* print(update.payload.dest)
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object
|
||||
* @param msg_id id of the message with the webxdc instance
|
||||
* @param status_update_id Can be used to filter out only a concrete status update.
|
||||
* When set to 0, all known status updates are returned.
|
||||
* @return JSON-array containing the requested updates,
|
||||
* each element was created by dc_send_webxdc_status_update()
|
||||
* on this or other devices.
|
||||
* If there are no updates, an empty JSON-array is returned.
|
||||
*/
|
||||
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t status_update_id);
|
||||
|
||||
/**
|
||||
* Save a draft for a chat in the database.
|
||||
*
|
||||
@@ -966,8 +1068,6 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
* @param msg The message to save as a draft.
|
||||
* Existing draft will be overwritten.
|
||||
* NULL deletes the existing draft, if any, without sending it.
|
||||
* Currently, also non-text-messages
|
||||
* will delete the existing drafts.
|
||||
*/
|
||||
void dc_set_draft (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg);
|
||||
|
||||
@@ -1383,7 +1483,6 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
|
||||
* Create a new group chat.
|
||||
*
|
||||
* After creation,
|
||||
* the draft of the chat is set to a default text,
|
||||
* the group has one member with the ID DC_CONTACT_ID_SELF
|
||||
* and is in _unpromoted_ state.
|
||||
* This means, you can add or remove members, change the name,
|
||||
@@ -1930,8 +2029,8 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
||||
|
||||
#define DC_IMEX_EXPORT_SELF_KEYS 1 // param1 is a directory where the keys are written to
|
||||
#define DC_IMEX_IMPORT_SELF_KEYS 2 // param1 is a directory where the keys are searched in and read from
|
||||
#define DC_IMEX_EXPORT_BACKUP 11 // param1 is a directory where the backup is written to
|
||||
#define DC_IMEX_IMPORT_BACKUP 12 // param1 is the file with the backup to import
|
||||
#define DC_IMEX_EXPORT_BACKUP 11 // param1 is a directory where the backup is written to, param2 is a passphrase to encrypt the backup
|
||||
#define DC_IMEX_IMPORT_BACKUP 12 // param1 is the file with the backup to import, param2 is the backup's passphrase
|
||||
|
||||
|
||||
/**
|
||||
@@ -1940,14 +2039,16 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
||||
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
|
||||
* What to do is defined by the _what_ parameter which may be one of the following:
|
||||
*
|
||||
* - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`.
|
||||
* - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`
|
||||
* encrypted with the passphrase given as `param2`. If `param2` is NULL or empty string,
|
||||
* the backup is not encrypted.
|
||||
* The backup contains all contacts, chats, images and other data and device independent settings.
|
||||
* The backup does not contain device dependent settings as ringtones or LED notification settings.
|
||||
* The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
|
||||
* the format is `delta-chat-<day>-<number>.tar`
|
||||
*
|
||||
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` 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
|
||||
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
|
||||
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
|
||||
* is only possible as long as the context is not configured or used in another way.
|
||||
*
|
||||
* - **DC_IMEX_EXPORT_SELF_KEYS** (1) - Export all private keys and all public keys of the user to the
|
||||
@@ -2243,6 +2344,20 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
char* dc_get_securejoin_qr (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Get QR code image from the QR code text generated by dc_get_securejoin_qr().
|
||||
* See dc_get_securejoin_qr() for details about the contained QR code.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id group-chat-id for secure-join or 0 for setup-contact,
|
||||
* see dc_get_securejoin_qr() for details.
|
||||
* @return SVG-Image with the QR code.
|
||||
* On errors, an empty string is returned.
|
||||
* The returned string must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
/**
|
||||
* Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
* started on another device with dc_get_securejoin_qr().
|
||||
@@ -2463,6 +2578,7 @@ void dc_str_unref (char* str);
|
||||
* To make this possible, some dc_context_t functions must not be called
|
||||
* when using the account manager:
|
||||
* - use dc_accounts_add_account() and dc_accounts_get_account() instead of dc_context_new()
|
||||
* - use dc_accounts_add_closed_account() instead of dc_context_new_closed()
|
||||
* - use dc_accounts_start_io() and dc_accounts_stop_io() instead of dc_start_io() and dc_stop_io()
|
||||
* - use dc_accounts_maybe_network() instead of dc_maybe_network()
|
||||
* - use dc_accounts_get_event_emitter() instead of dc_get_event_emitter()
|
||||
@@ -2520,6 +2636,22 @@ void dc_accounts_unref (dc_accounts_t* accounts);
|
||||
*/
|
||||
uint32_t dc_accounts_add_account (dc_accounts_t* accounts);
|
||||
|
||||
/**
|
||||
* Add a new closed account to the account manager.
|
||||
* Internally, dc_context_new_closed() is called using a unique database-name
|
||||
* in the directory specified at dc_accounts_new().
|
||||
*
|
||||
* If the function succeeds,
|
||||
* dc_accounts_get_all() will return one more account
|
||||
* and you can access the newly created account using dc_accounts_get_account().
|
||||
* Moreover, the newly created account will be the selected one.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
* @return Account-id, use dc_accounts_get_account() to get the context object.
|
||||
* On errors, 0 is returned.
|
||||
*/
|
||||
uint32_t dc_accounts_add_closed_account (dc_accounts_t* accounts);
|
||||
|
||||
/**
|
||||
* Migrate independent accounts into accounts managed by the account manager.
|
||||
@@ -3551,6 +3683,45 @@ char* dc_msg_get_filename (const dc_msg_t* msg);
|
||||
char* dc_msg_get_filemime (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Return file from inside an webxdc message.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The webxdc instance.
|
||||
* @param filename The name inside the archive,
|
||||
* can be given as an absolute path (`/file.png`)
|
||||
* or as a relative path (`file.png`, no leading slash)
|
||||
* @param ret_bytes Pointer to a size_t. The size of the blob will be written here.
|
||||
* @return The blob must be released using dc_str_unref() after usage.
|
||||
* NULL if there is no such file in the archive or on errors.
|
||||
*/
|
||||
char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char* filename, size_t* ret_bytes);
|
||||
|
||||
|
||||
/**
|
||||
* Get info from a webxdc message, in JSON format.
|
||||
* The returned JSON string has the following key/values:
|
||||
*
|
||||
* - name: The name of the app.
|
||||
* Defaults to the filename if not set in the manifest.
|
||||
* - icon: App icon file name.
|
||||
* Defaults to an standard icon if nothing is set in the manifest.
|
||||
* To get the file, use dc_msg_get_webxdc_blob().
|
||||
* App icons should should be square,
|
||||
* the implementations will add round corners etc. as needed.
|
||||
* - summary: short string describing the state of the app,
|
||||
* sth. as "2 votes", "Highscore: 123",
|
||||
* can be changed by the apps and defaults to an empty string.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The webxdc instance.
|
||||
* @return a UTF8-encoded JSON string containing all requested info.
|
||||
* Must be freed using dc_str_unref().
|
||||
* NULL is never returned.
|
||||
*/
|
||||
char* dc_msg_get_webxdc_info (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get the size of the file. Returns the size of the file associated with a
|
||||
* message, if applicable.
|
||||
@@ -3802,8 +3973,11 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
|
||||
* These messages are typically shown in the center of the chat view,
|
||||
* dc_msg_get_text() returns a descriptive text about what is going on.
|
||||
*
|
||||
* For informational messages created by Webxdc apps,
|
||||
* dc_msg_get_parent() usually returns the Webxdc instance;
|
||||
* UIs can use that to scroll to the Webxdc app when the info is tapped.
|
||||
*
|
||||
* There is no need to perform any action when seeing such a message - this is already done by the core.
|
||||
* Typically, these messages are displayed in the center of the chat.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
@@ -4158,7 +4332,8 @@ void dc_msg_latefiling_mediasize (dc_msg_t* msg, int width, int hei
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object to set the reply to.
|
||||
* @param quote The quote to set for msg.
|
||||
* @param quote The quote to set for the message object given as `msg`.
|
||||
* NULL removes an previously set quote.
|
||||
*/
|
||||
void dc_msg_set_quote (dc_msg_t* msg, const dc_msg_t* quote);
|
||||
|
||||
@@ -4203,6 +4378,32 @@ char* dc_msg_get_quoted_text (const dc_msg_t* msg);
|
||||
*/
|
||||
dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
|
||||
|
||||
/**
|
||||
* Get parent message, if available.
|
||||
*
|
||||
* Used for Webxdc-info-messages
|
||||
* to jump to the corresponding instance that created the info message.
|
||||
*
|
||||
* For quotes, please use the more specialized
|
||||
* dc_msg_get_quoted_text() and dc_msg_get_quoted_msg().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return The parent message or NULL.
|
||||
* Must be freed using dc_msg_unref() after usage.
|
||||
*/
|
||||
dc_msg_t* dc_msg_get_parent (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Force the message to be sent in plain text.
|
||||
*
|
||||
* This API is for bots, there is no need to expose it in the UI.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
*/
|
||||
void dc_msg_force_plaintext (dc_msg_t* msg);
|
||||
|
||||
/**
|
||||
* @class dc_contact_t
|
||||
@@ -4382,6 +4583,16 @@ uint32_t dc_contact_get_color (const dc_contact_t* contact);
|
||||
*/
|
||||
char* dc_contact_get_status (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Get the contact's last seen timestamp.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return Last seen timestamp.
|
||||
* 0 on error or if the contact was never seen.
|
||||
*/
|
||||
int64_t dc_contact_get_last_seen (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Check if a contact is blocked.
|
||||
*
|
||||
@@ -4688,6 +4899,15 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*/
|
||||
#define DC_MSG_VIDEOCHAT_INVITATION 70
|
||||
|
||||
|
||||
/**
|
||||
* The message is a webxdc instance.
|
||||
*
|
||||
* To send data to a webxdc instance, use dc_send_webxdc_status_update()
|
||||
*/
|
||||
#define DC_MSG_WEBXDC 80
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -5381,6 +5601,21 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_SELFAVATAR_CHANGED 2110
|
||||
|
||||
|
||||
/**
|
||||
* webxdc status update received.
|
||||
* To get the received status update, use dc_get_webxdc_status_updates().
|
||||
* To send status updates, use dc_send_webxdc_status_update().
|
||||
*
|
||||
* Note, that you do not get events that arrive when the app is not running;
|
||||
* instead, you can use dc_get_webxdc_status_updates() to get all status updates
|
||||
* and catch up that way.
|
||||
*
|
||||
* @param data1 (int) msg_id
|
||||
* @param data2 (int) status_update_id
|
||||
*/
|
||||
#define DC_EVENT_WEBXDC_STATUS_UPDATE 2120
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -5607,18 +5842,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in summaries.
|
||||
#define DC_STR_FILE 12
|
||||
|
||||
/// "Sent with my Delta Chat Messenger: https://delta.chat"
|
||||
///
|
||||
/// Used as the default footer
|
||||
/// if nothing else is set by the dc_set_config()-option `selfstatus`.
|
||||
#define DC_STR_STATUSLINE 13
|
||||
|
||||
/// "Hi, i've created the group %1$s for us."
|
||||
///
|
||||
/// Used as a draft text after group creation.
|
||||
/// - %1$s will be replaced by the group name
|
||||
#define DC_STR_NEWGROUPDRAFT 14
|
||||
|
||||
/// "Group name changed from %1$s to %2$s."
|
||||
///
|
||||
/// Used in status messages for group name changes.
|
||||
@@ -6073,6 +6296,20 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by the name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_REPLIES 118
|
||||
|
||||
/// "Scan to chat with %1$s"
|
||||
///
|
||||
/// Subtitle for verification qrcode svg image generated by the core.
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the inviter.
|
||||
#define DC_STR_SETUP_CONTACT_QR_DESC 119
|
||||
|
||||
/// "Scan to join %1$s"
|
||||
///
|
||||
/// Subtitle for group join qrcode svg image generated by the core.
|
||||
///
|
||||
/// `%1$s` will be replaced with the group name.
|
||||
#define DC_STR_SECURE_JOIN_GROUP_QR_DESC 120
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -25,7 +25,9 @@ use std::time::{Duration, SystemTime};
|
||||
use anyhow::Context as _;
|
||||
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;
|
||||
@@ -35,6 +37,7 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateId;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
|
||||
@@ -62,7 +65,7 @@ pub type dc_context_t = Context;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_new(
|
||||
os_name: *const libc::c_char,
|
||||
_os_name: *const libc::c_char,
|
||||
dbfile: *const libc::c_char,
|
||||
blobdir: *const libc::c_char,
|
||||
) -> *mut dc_context_t {
|
||||
@@ -73,21 +76,10 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let os_name = if os_name.is_null() {
|
||||
String::from("DcFFI")
|
||||
} else {
|
||||
to_string_lossy(os_name)
|
||||
};
|
||||
|
||||
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(
|
||||
os_name,
|
||||
as_path(dbfile).to_path_buf().into(),
|
||||
id,
|
||||
))
|
||||
block_on(Context::new(as_path(dbfile).to_path_buf().into(), id))
|
||||
} else {
|
||||
eprintln!("blobdir can not be defined explicitly anymore");
|
||||
return ptr::null_mut();
|
||||
@@ -95,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.
|
||||
@@ -192,7 +235,7 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
.unwrap_or_default()
|
||||
.strdup(),
|
||||
Err(_) => {
|
||||
warn!(ctx, "dc_get_config(): invalid key");
|
||||
warn!(ctx, "dc_get_config(): invalid key '{}'", &key);
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
@@ -458,6 +501,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
EventType::ImexFileWritten(_) => 0,
|
||||
EventType::SecurejoinInviterProgress { contact_id, .. }
|
||||
| EventType::SecurejoinJoinerProgress { contact_id, .. } => *contact_id as libc::c_int,
|
||||
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,6 +543,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
EventType::SecurejoinInviterProgress { progress, .. }
|
||||
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||
EventType::WebxdcStatusUpdate {
|
||||
status_update_id, ..
|
||||
} => status_update_id.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,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 {
|
||||
@@ -641,8 +689,8 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let public = key::SignedPublicKey::from_base64(&to_string_lossy(public_data))?;
|
||||
let secret = key::SignedSecretKey::from_base64(&to_string_lossy(secret_data))?;
|
||||
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
|
||||
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
|
||||
let keypair = key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
@@ -829,6 +877,52 @@ 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,
|
||||
status_update_id: 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),
|
||||
if status_update_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(StatusUpdateId::new(status_update_id))
|
||||
},
|
||||
))
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1736,7 +1830,7 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
Contact::lookup_id_by_addr(ctx, to_string_lossy(addr), Origin::IncomingReplyTo)
|
||||
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)
|
||||
@@ -1928,7 +2022,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()");
|
||||
@@ -1941,12 +2035,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")
|
||||
});
|
||||
@@ -2075,6 +2170,27 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_securejoin_qr_svg(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to generate_verification_qr()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
let chat_id = if chat_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
|
||||
block_on(get_securejoin_qr_svg(ctx, chat_id))
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_join_securejoin(
|
||||
context: *mut dc_context_t,
|
||||
@@ -2431,7 +2547,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]
|
||||
@@ -2948,6 +3071,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() {
|
||||
@@ -3360,17 +3538,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();
|
||||
@@ -3413,6 +3595,39 @@ 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() {
|
||||
eprintln!("ignoring careless call to dc_msg_force_plaintext()");
|
||||
return;
|
||||
}
|
||||
let ffi_msg = &mut *msg;
|
||||
ffi_msg.message.force_plaintext();
|
||||
}
|
||||
|
||||
// dc_contact_t
|
||||
|
||||
/// FFI struct for [dc_contact_t]
|
||||
@@ -3544,6 +3759,16 @@ pub unsafe extern "C" fn dc_contact_get_status(contact: *mut dc_contact_t) -> *m
|
||||
ffi_contact.contact.get_status().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_get_last_seen(contact: *mut dc_contact_t) -> i64 {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_last_seen()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.last_seen()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
@@ -3721,7 +3946,11 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
|
||||
match socks5_enabled {
|
||||
Ok(socks5_enabled) => {
|
||||
match block_on(provider::get_provider_info(addr.as_str(), socks5_enabled)) {
|
||||
match block_on(provider::get_provider_info(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
socks5_enabled,
|
||||
)) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -3803,7 +4032,7 @@ pub type dc_accounts_t = AccountsWrapper;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_new(
|
||||
os_name: *const libc::c_char,
|
||||
_os_name: *const libc::c_char,
|
||||
dbfile: *const libc::c_char,
|
||||
) -> *mut dc_accounts_t {
|
||||
setup_panic!();
|
||||
@@ -3813,13 +4042,7 @@ pub unsafe extern "C" fn dc_accounts_new(
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let os_name = if os_name.is_null() {
|
||||
String::from("DcFFI")
|
||||
} else {
|
||||
to_string_lossy(os_name)
|
||||
};
|
||||
|
||||
let accs = block_on(Accounts::new(os_name, as_path(dbfile).to_path_buf().into()));
|
||||
let accs = block_on(Accounts::new(as_path(dbfile).to_path_buf().into()));
|
||||
|
||||
match accs {
|
||||
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
||||
@@ -3924,6 +4147,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,
|
||||
|
||||
33
draft/aeap-mvp.rst
Normal file
33
draft/aeap-mvp.rst
Normal file
@@ -0,0 +1,33 @@
|
||||
AEAP MVP
|
||||
========
|
||||
|
||||
Changes to the UIs
|
||||
------------------
|
||||
|
||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
||||
|
||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
||||
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- No changes for 1:1 chats, there simply is a new one
|
||||
|
||||
- When we send a message to a group, and the primary address is not a member of a group, but a secondary address is:
|
||||
|
||||
Add Chat-Group-Member-Removed=<old address> and Chat-Group-Member-Added=<new address> headers to this message
|
||||
|
||||
- On the receiving side, make sure that we accept this (even in verified groups) if the message is signed and the key stayed the same
|
||||
|
||||
Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
194
draft/webxdc-dev-reference.md
Normal file
194
draft/webxdc-dev-reference.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# 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 fields:
|
||||
- `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. contact requests 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()` nor by `getAllUpdates()`.
|
||||
|
||||
|
||||
### setUpdateListener()
|
||||
|
||||
```js
|
||||
window.webxdc.setUpdateListener((update) => {});
|
||||
```
|
||||
|
||||
With `setUpdateListener()` you define a callback that receives the updates
|
||||
sent by `sendUpdate()`.
|
||||
|
||||
- `update`: passed to the callback on updates with the following fields:
|
||||
`update.payload`: equals the payload given to `sendUpdate()`
|
||||
|
||||
The callback is called for updates sent by you or other peers.
|
||||
|
||||
|
||||
### getAllUpdates()
|
||||
|
||||
```js
|
||||
updates = await window.webxdc.getAllUpdates();
|
||||
```
|
||||
|
||||
In case your Webxdc was just started,
|
||||
you may want to reconstruct the state from the last run -
|
||||
and also incorporate updates that may have arrived while the app was not running.
|
||||
|
||||
- `updates`: All previous updates in an array,
|
||||
eg. `[{payload: "foo"},{payload: "bar"}]`
|
||||
if `webxdc.sendUpdate({payload: "foo"}); webxdc.sendUpdate({payload: "bar"};` was called on the last run.
|
||||
|
||||
The updates are wrapped into a Promise that you can `await` for.
|
||||
If you are not in an async function and cannot use `await` therefore,
|
||||
you can get the updates with `then()`:
|
||||
|
||||
```js
|
||||
window.webxdc.getAllUpdates().then(updates => {});
|
||||
```
|
||||
|
||||
|
||||
### 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);
|
||||
window.webxdc.getAllUpdates().then(updates => updates.forEach(receiveUpdate));
|
||||
|
||||
</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.
|
||||
@@ -101,7 +101,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
|
||||
let data = dc_read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false).await {
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", false).await {
|
||||
println!("dc_receive_imf errored: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
@@ -387,6 +387,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\
|
||||
@@ -423,6 +424,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
listblocked\n\
|
||||
======================================Misc.==\n\
|
||||
getqr [<chat-id>]\n\
|
||||
getqrsvg [<chat-id>]\n\
|
||||
getbadqr\n\
|
||||
checkqr <qr-content>\n\
|
||||
joinqr <qr-content>\n\
|
||||
@@ -470,20 +472,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);
|
||||
@@ -562,7 +576,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),
|
||||
@@ -754,7 +768,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"groupname" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
chat::set_chat_name(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
|
||||
chat::set_chat_name(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
&format!("{} {}", arg1, arg2).trim(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Chat name set");
|
||||
}
|
||||
@@ -901,6 +920,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?;
|
||||
@@ -1136,7 +1165,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());
|
||||
}
|
||||
}
|
||||
@@ -1179,7 +1208,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let socks5_enabled = context
|
||||
.get_config_bool(config::Config::Socks5Enabled)
|
||||
.await?;
|
||||
match provider::get_provider_info(arg1, socks5_enabled).await {
|
||||
match provider::get_provider_info(&context, arg1, socks5_enabled).await {
|
||||
Some(info) => {
|
||||
println!("Information for provider belonging to {}:", arg1);
|
||||
println!("status: {}", info.status as u32);
|
||||
|
||||
@@ -33,6 +33,8 @@ use rustyline::{
|
||||
|
||||
mod cmdline;
|
||||
use self::cmdline::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use std::fs;
|
||||
|
||||
/// Event Handler
|
||||
fn receive_event(event: EventType) {
|
||||
@@ -167,7 +169,7 @@ const DB_COMMANDS: [&str; 10] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 35] = [
|
||||
const CHAT_COMMANDS: [&str; 36] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
@@ -189,6 +191,7 @@ const CHAT_COMMANDS: [&str; 35] = [
|
||||
"sendfile",
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"sendupdate",
|
||||
"videochat",
|
||||
"draft",
|
||||
"listmedia",
|
||||
@@ -224,8 +227,9 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 11] = [
|
||||
const MISC_COMMANDS: [&str; 12] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
@@ -294,7 +298,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf(), 0).await?;
|
||||
let context = Context::new(Path::new(&args[1]).to_path_buf(), 0).await?;
|
||||
|
||||
let events = context.get_event_emitter();
|
||||
async_std::task::spawn(async move {
|
||||
@@ -427,6 +431,20 @@ async fn handle_cmd(
|
||||
io::stderr().write_all(&output.stderr).unwrap();
|
||||
}
|
||||
}
|
||||
"getqrsvg" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
match get_securejoin_qr_svg(&ctx, group).await {
|
||||
Ok(svg) => {
|
||||
fs::write(&file, svg)?;
|
||||
println!("QR code svg written to: {:#?}", file);
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Failed to get QR code svg: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
"joinqr" => {
|
||||
ctx.start_io().await;
|
||||
if !arg0.is_empty() {
|
||||
|
||||
@@ -36,7 +36,7 @@ async fn main() {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
log::info!("creating database {:?}", dbfile);
|
||||
let ctx = Context::new("FakeOs".into(), dbfile.into(), 0)
|
||||
let ctx = Context::new(dbfile.into(), 0)
|
||||
.await
|
||||
.expect("Failed to create context");
|
||||
let info = ctx.get_info().await;
|
||||
|
||||
19
python/mypy.ini
Normal file
19
python/mypy.ini
Normal file
@@ -0,0 +1,19 @@
|
||||
[mypy]
|
||||
|
||||
[mypy-deltachat.capi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pluggy.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-cffi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-imapclient.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pytest.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-_pytest.*]
|
||||
ignore_missing_imports = True
|
||||
@@ -1,5 +1,6 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
|
||||
@@ -12,6 +12,7 @@ def main():
|
||||
long_description=long_description,
|
||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient', 'requests'],
|
||||
setup_requires=['setuptools_scm'], # required for compatibility with `python3 setup.py sdist`
|
||||
packages=setuptools.find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||
|
||||
@@ -19,9 +19,9 @@ except DistributionNotFound:
|
||||
|
||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
if not _DC_EVENTNAME_MAP:
|
||||
for name, val in vars(const).items():
|
||||
for name in dir(const):
|
||||
if name.startswith("DC_EVENT_"):
|
||||
_DC_EVENTNAME_MAP[val] = name
|
||||
_DC_EVENTNAME_MAP[getattr(const, name)] = name
|
||||
return _DC_EVENTNAME_MAP[integer]
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ def run_cmdline(argv=None, account_plugins=None):
|
||||
ac.set_config("addr", args.email)
|
||||
ac.set_config("mail_pw", args.password)
|
||||
ac.set_config("mvbox_move", "0")
|
||||
ac.set_config("mvbox_watch", "0")
|
||||
ac.set_config("sentbox_watch", "0")
|
||||
ac.set_config("bot", "1")
|
||||
configtracker = ac.configure()
|
||||
|
||||
@@ -8,13 +8,14 @@ import os
|
||||
from array import array
|
||||
from . import const
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array, DCLot
|
||||
from .chat import Chat
|
||||
from .message import Message
|
||||
from .contact import Contact
|
||||
from .tracker import ImexTracker, ConfigureTracker
|
||||
from . import hookspec
|
||||
from .events import EventThread
|
||||
from typing import Union, Any, Dict, Optional, List, Generator
|
||||
|
||||
|
||||
class MissingCredentials(ValueError):
|
||||
@@ -28,7 +29,7 @@ class Account(object):
|
||||
"""
|
||||
MissingCredentials = MissingCredentials
|
||||
|
||||
def __init__(self, db_path, os_name=None, logging=True):
|
||||
def __init__(self, db_path, os_name=None, logging=True) -> None:
|
||||
""" initialize account object.
|
||||
|
||||
:param db_path: a path to the account database. The database
|
||||
@@ -58,11 +59,11 @@ class Account(object):
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
hook.dc_account_init(account=self)
|
||||
|
||||
def disable_logging(self):
|
||||
def disable_logging(self) -> None:
|
||||
""" disable logging. """
|
||||
self._logging = False
|
||||
|
||||
def enable_logging(self):
|
||||
def enable_logging(self) -> None:
|
||||
""" re-enable logging. """
|
||||
self._logging = True
|
||||
|
||||
@@ -73,12 +74,12 @@ class Account(object):
|
||||
if self._logging:
|
||||
self._pm.hook.ac_log_line(message=msg)
|
||||
|
||||
def _check_config_key(self, name):
|
||||
def _check_config_key(self, name: str) -> None:
|
||||
if name not in self._configkeys:
|
||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||
name, self._configkeys))
|
||||
|
||||
def get_info(self):
|
||||
def get_info(self) -> Dict[str, str]:
|
||||
""" return dictionary of built config parameters. """
|
||||
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||
d = {}
|
||||
@@ -105,19 +106,19 @@ class Account(object):
|
||||
cursor += len(entry) + 1
|
||||
log("")
|
||||
|
||||
def set_stock_translation(self, id, string):
|
||||
def set_stock_translation(self, id: int, string: str) -> None:
|
||||
""" set stock translation string.
|
||||
|
||||
:param id: id of stock string (const.DC_STR_*)
|
||||
:param value: string to set as new transalation
|
||||
:returns: None
|
||||
"""
|
||||
string = string.encode("utf8")
|
||||
res = lib.dc_set_stock_translation(self._dc_context, id, string)
|
||||
bytestring = string.encode("utf8")
|
||||
res = lib.dc_set_stock_translation(self._dc_context, id, bytestring)
|
||||
if res == 0:
|
||||
raise ValueError("could not set translation string")
|
||||
|
||||
def set_config(self, name, value):
|
||||
def set_config(self, name: str, value: Optional[str]) -> None:
|
||||
""" set configuration values.
|
||||
|
||||
:param name: config key name (unicode)
|
||||
@@ -125,16 +126,16 @@ class Account(object):
|
||||
:returns: None
|
||||
"""
|
||||
self._check_config_key(name)
|
||||
name = name.encode("utf8")
|
||||
if name == b"addr" and self.is_configured():
|
||||
namebytes = name.encode("utf8")
|
||||
if namebytes == b"addr" and self.is_configured():
|
||||
raise ValueError("can not change 'addr' after account is configured.")
|
||||
if value is not None:
|
||||
value = value.encode("utf8")
|
||||
valuebytes = value.encode("utf8")
|
||||
else:
|
||||
value = ffi.NULL
|
||||
lib.dc_set_config(self._dc_context, name, value)
|
||||
valuebytes = ffi.NULL
|
||||
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
|
||||
|
||||
def get_config(self, name):
|
||||
def get_config(self, name: str) -> str:
|
||||
""" return unicode string value.
|
||||
|
||||
:param name: configuration key to lookup (eg "addr" or "mail_pw")
|
||||
@@ -143,12 +144,12 @@ class Account(object):
|
||||
"""
|
||||
if name != "sys.config_keys":
|
||||
self._check_config_key(name)
|
||||
name = name.encode("utf8")
|
||||
res = lib.dc_get_config(self._dc_context, name)
|
||||
namebytes = name.encode("utf8")
|
||||
res = lib.dc_get_config(self._dc_context, namebytes)
|
||||
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def _preconfigure_keypair(self, addr, public, secret):
|
||||
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
|
||||
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||
|
||||
In other words, you don't need this.
|
||||
@@ -160,7 +161,7 @@ class Account(object):
|
||||
if res == 0:
|
||||
raise Exception("Failed to set key")
|
||||
|
||||
def update_config(self, kwargs):
|
||||
def update_config(self, kwargs: Dict[str, Any]) -> None:
|
||||
""" update config values.
|
||||
|
||||
:param kwargs: name=value config settings for this account.
|
||||
@@ -170,7 +171,7 @@ class Account(object):
|
||||
for key, value in kwargs.items():
|
||||
self.set_config(key, str(value))
|
||||
|
||||
def is_configured(self):
|
||||
def is_configured(self) -> bool:
|
||||
""" determine if the account is configured already; an initial connection
|
||||
to SMTP/IMAP has been verified.
|
||||
|
||||
@@ -178,7 +179,13 @@ class Account(object):
|
||||
"""
|
||||
return True if lib.dc_is_configured(self._dc_context) else False
|
||||
|
||||
def set_avatar(self, img_path):
|
||||
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.
|
||||
|
||||
:raises ValueError: if profile image could not be set
|
||||
@@ -190,20 +197,18 @@ class Account(object):
|
||||
assert os.path.exists(img_path), img_path
|
||||
self.set_config("selfavatar", img_path)
|
||||
|
||||
def check_is_configured(self):
|
||||
def check_is_configured(self) -> None:
|
||||
""" Raise ValueError if this account is not configured. """
|
||||
if not self.is_configured():
|
||||
raise ValueError("need to configure first")
|
||||
|
||||
def get_latest_backupfile(self, backupdir):
|
||||
def get_latest_backupfile(self, backupdir) -> Optional[str]:
|
||||
""" return the latest backup file in a given directory.
|
||||
"""
|
||||
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
|
||||
if res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(res)
|
||||
return from_optional_dc_charpointer(res)
|
||||
|
||||
def get_blobdir(self):
|
||||
def get_blobdir(self) -> str:
|
||||
""" return the directory for files.
|
||||
|
||||
All sent files are copied to this directory if necessary.
|
||||
@@ -211,15 +216,15 @@ class Account(object):
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
|
||||
|
||||
def get_self_contact(self):
|
||||
def get_self_contact(self) -> Contact:
|
||||
""" return this account's identity as a :class:`deltachat.contact.Contact`.
|
||||
|
||||
:returns: :class:`deltachat.contact.Contact`
|
||||
"""
|
||||
return Contact(self, const.DC_CONTACT_ID_SELF)
|
||||
|
||||
def create_contact(self, obj, name=None):
|
||||
""" create a (new) Contact or return an existing one.
|
||||
def create_contact(self, obj, name: Optional[str] = None) -> Contact:
|
||||
"""create a (new) Contact or return an existing one.
|
||||
|
||||
Calling this method will always result in the same
|
||||
underlying contact id. If there already is a Contact
|
||||
@@ -236,13 +241,13 @@ class Account(object):
|
||||
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact(self, obj):
|
||||
def get_contact(self, obj) -> Optional[Contact]:
|
||||
if isinstance(obj, Contact):
|
||||
return obj
|
||||
(_, addr) = self.get_contact_addr_and_name(obj)
|
||||
return self.get_contact_by_addr(addr)
|
||||
|
||||
def get_contact_addr_and_name(self, obj, name=None):
|
||||
def get_contact_addr_and_name(self, obj, name: Optional[str] = None):
|
||||
if isinstance(obj, Account):
|
||||
if not obj.is_configured():
|
||||
raise ValueError("can only add addresses from configured accounts")
|
||||
@@ -260,7 +265,7 @@ class Account(object):
|
||||
name = displayname
|
||||
return (name, addr)
|
||||
|
||||
def delete_contact(self, contact):
|
||||
def delete_contact(self, contact: Contact) -> bool:
|
||||
""" delete a Contact.
|
||||
|
||||
:param contact: contact object obtained
|
||||
@@ -271,22 +276,23 @@ class Account(object):
|
||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||
|
||||
def get_contact_by_addr(self, email):
|
||||
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
||||
""" get a contact for the email address or None if it's blocked or doesn't exist. """
|
||||
_, addr = parseaddr(email)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||
if contact_id:
|
||||
return self.get_contact_by_id(contact_id)
|
||||
return None
|
||||
|
||||
def get_contact_by_id(self, contact_id):
|
||||
""" return Contact instance or None.
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
""" return Contact instance or raise an exception.
|
||||
:param contact_id: integer id of this contact.
|
||||
:returns: None or :class:`deltachat.contact.Contact` instance.
|
||||
:returns: :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_blocked_contacts(self):
|
||||
def get_blocked_contacts(self) -> List[Contact]:
|
||||
""" return a list of all blocked contacts.
|
||||
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects.
|
||||
@@ -297,8 +303,13 @@ class Account(object):
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
||||
""" get a (filtered) list of contacts.
|
||||
def get_contacts(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
with_self: bool = False,
|
||||
only_verified: bool = False,
|
||||
) -> List[Contact]:
|
||||
"""get a (filtered) list of contacts.
|
||||
|
||||
:param query: if a string is specified, only return contacts
|
||||
whose name or e-mail matches query.
|
||||
@@ -318,7 +329,7 @@ class Account(object):
|
||||
)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_fresh_messages(self):
|
||||
def get_fresh_messages(self) -> Generator[Message, None, None]:
|
||||
""" yield all fresh messages from all chats. """
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_fresh_msgs(self._dc_context),
|
||||
@@ -326,12 +337,17 @@ class Account(object):
|
||||
)
|
||||
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
|
||||
|
||||
def create_chat(self, obj):
|
||||
def create_chat(self, obj) -> Chat:
|
||||
""" Create a 1:1 chat with Account, Contact or e-mail address. """
|
||||
return self.create_contact(obj).create_chat()
|
||||
|
||||
def create_group_chat(self, name, contacts=None, verified=False):
|
||||
""" create a new group chat object.
|
||||
def create_group_chat(
|
||||
self,
|
||||
name: str,
|
||||
contacts: Optional[List[Contact]] = None,
|
||||
verified: bool = False,
|
||||
) -> Chat:
|
||||
"""create a new group chat object.
|
||||
|
||||
Chats are unpromoted until the first message is sent.
|
||||
|
||||
@@ -347,7 +363,7 @@ class Account(object):
|
||||
chat.add_contact(contact)
|
||||
return chat
|
||||
|
||||
def get_chats(self):
|
||||
def get_chats(self) -> List[Chat]:
|
||||
""" return list of chats.
|
||||
|
||||
:returns: a list of :class:`deltachat.chat.Chat` objects.
|
||||
@@ -364,17 +380,17 @@ class Account(object):
|
||||
chatlist.append(Chat(self, chat_id))
|
||||
return chatlist
|
||||
|
||||
def get_device_chat(self):
|
||||
def get_device_chat(self) -> Chat:
|
||||
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
|
||||
|
||||
def get_message_by_id(self, msg_id):
|
||||
def get_message_by_id(self, msg_id: int) -> Message:
|
||||
""" return Message instance.
|
||||
:param msg_id: integer id of this message.
|
||||
:returns: :class:`deltachat.message.Message` instance.
|
||||
"""
|
||||
return Message.from_db(self, msg_id)
|
||||
|
||||
def get_chat_by_id(self, chat_id):
|
||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||
""" return Chat instance.
|
||||
:param chat_id: integer id of this chat.
|
||||
:returns: :class:`deltachat.chat.Chat` instance.
|
||||
@@ -386,19 +402,21 @@ class Account(object):
|
||||
lib.dc_chat_unref(res)
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def mark_seen_messages(self, messages):
|
||||
def mark_seen_messages(self, messages: List[Union[int, Message]]) -> None:
|
||||
""" mark the given set of messages as seen.
|
||||
|
||||
:param messages: a list of message ids or Message instances.
|
||||
"""
|
||||
arr = array("i")
|
||||
for msg in messages:
|
||||
msg = getattr(msg, "id", msg)
|
||||
arr.append(msg)
|
||||
if isinstance(msg, Message):
|
||||
arr.append(msg.id)
|
||||
else:
|
||||
arr.append(msg)
|
||||
msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr))
|
||||
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
|
||||
|
||||
def forward_messages(self, messages, chat):
|
||||
def forward_messages(self, messages: List[Message], chat: Chat) -> None:
|
||||
""" Forward list of messages to a chat.
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
@@ -408,7 +426,7 @@ class Account(object):
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
|
||||
|
||||
def delete_messages(self, messages):
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
""" delete messages (local and remote).
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
@@ -466,7 +484,7 @@ class Account(object):
|
||||
def imex(self, path, imex_cmd):
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
|
||||
def initiate_key_transfer(self):
|
||||
def initiate_key_transfer(self) -> str:
|
||||
"""return setup code after a Autocrypt setup message
|
||||
has been successfully sent to our own e-mail address ("self-sent message").
|
||||
If sending out was unsuccessful, a RuntimeError is raised.
|
||||
@@ -477,7 +495,7 @@ class Account(object):
|
||||
raise RuntimeError("could not send out autocrypt setup message")
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_setup_contact_qr(self):
|
||||
def get_setup_contact_qr(self) -> str:
|
||||
""" get/create Setup-Contact QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
@@ -527,7 +545,9 @@ class Account(object):
|
||||
raise ValueError("could not join group")
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||
def set_location(
|
||||
self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0
|
||||
) -> None:
|
||||
"""set a new location. It effects all chats where we currently
|
||||
have enabled location streaming.
|
||||
|
||||
@@ -571,7 +591,7 @@ class Account(object):
|
||||
def get_connectivity(self):
|
||||
return lib.dc_get_connectivity(self._dc_context)
|
||||
|
||||
def get_connectivity_html(self):
|
||||
def get_connectivity_html(self) -> str:
|
||||
return from_dc_charpointer(lib.dc_get_connectivity_html(self._dc_context))
|
||||
|
||||
def all_work_done(self):
|
||||
@@ -621,7 +641,7 @@ class Account(object):
|
||||
"""
|
||||
lib.dc_maybe_network(self._dc_context)
|
||||
|
||||
def configure(self, reconfigure=False):
|
||||
def configure(self, reconfigure: bool = False) -> ConfigureTracker:
|
||||
""" Start configuration process and return a Configtracker instance
|
||||
on which you can block with wait_finish() to get a True/False success
|
||||
value for the configuration process.
|
||||
@@ -634,11 +654,11 @@ class Account(object):
|
||||
lib.dc_configure(self._dc_context)
|
||||
return configtracker
|
||||
|
||||
def wait_shutdown(self):
|
||||
def wait_shutdown(self) -> None:
|
||||
""" wait until shutdown of this account has completed. """
|
||||
self._shutdown_event.wait()
|
||||
|
||||
def stop_io(self):
|
||||
def stop_io(self) -> None:
|
||||
""" stop core IO scheduler if it is running. """
|
||||
self.log("stop_ongoing")
|
||||
self.stop_ongoing()
|
||||
@@ -646,7 +666,7 @@ class Account(object):
|
||||
self.log("dc_stop_io (stop core IO scheduler)")
|
||||
lib.dc_stop_io(self._dc_context)
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self) -> None:
|
||||
""" shutdown and destroy account (stop callback thread, close and remove
|
||||
underlying dc_context)."""
|
||||
if self._dc_context is None:
|
||||
|
||||
@@ -9,6 +9,7 @@ from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from .message import Message
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Chat(object):
|
||||
@@ -17,20 +18,20 @@ class Chat(object):
|
||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||
"""
|
||||
|
||||
def __init__(self, account, id):
|
||||
def __init__(self, account, id) -> None:
|
||||
from .account import Account
|
||||
assert isinstance(account, Account), repr(account)
|
||||
self.account = account
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> bool:
|
||||
return self.id == getattr(other, "id", None) and \
|
||||
self.account._dc_context == other.account._dc_context
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other) -> bool:
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "<Chat id={} name={}>".format(self.id, self.get_name())
|
||||
|
||||
@property
|
||||
@@ -40,7 +41,7 @@ class Chat(object):
|
||||
lib.dc_chat_unref
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
def delete(self) -> None:
|
||||
"""Delete this chat and all its messages.
|
||||
|
||||
Note:
|
||||
@@ -50,24 +51,24 @@ class Chat(object):
|
||||
"""
|
||||
lib.dc_delete_chat(self.account._dc_context, self.id)
|
||||
|
||||
def block(self):
|
||||
def block(self) -> None:
|
||||
"""Block this chat."""
|
||||
lib.dc_block_chat(self.account._dc_context, self.id)
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
"""Accept this contact request chat."""
|
||||
lib.dc_accept_chat(self.account._dc_context, self.id)
|
||||
|
||||
# ------ chat status/metadata API ------------------------------
|
||||
|
||||
def is_group(self):
|
||||
def is_group(self) -> bool:
|
||||
""" return true if this chat is a group chat.
|
||||
|
||||
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
||||
|
||||
def is_muted(self):
|
||||
def is_muted(self) -> bool:
|
||||
""" return true if this chat is muted.
|
||||
|
||||
:returns: True if chat is muted, False otherwise.
|
||||
@@ -90,7 +91,7 @@ class Chat(object):
|
||||
"""
|
||||
return not lib.dc_chat_is_unpromoted(self._dc_chat)
|
||||
|
||||
def can_send(self):
|
||||
def can_send(self) -> bool:
|
||||
"""Check if messages can be sent to a give chat.
|
||||
This is not true eg. for the contact requests or for the device-talk
|
||||
|
||||
@@ -98,30 +99,30 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_chat_can_send(self._dc_chat)
|
||||
|
||||
def is_protected(self):
|
||||
def is_protected(self) -> bool:
|
||||
""" return True if this chat is a protected chat.
|
||||
|
||||
:returns: True if chat is protected, False otherwise.
|
||||
"""
|
||||
return lib.dc_chat_is_protected(self._dc_chat)
|
||||
|
||||
def get_name(self):
|
||||
def get_name(self) -> Optional[str]:
|
||||
""" return name of this chat.
|
||||
|
||||
:returns: unicode name
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat))
|
||||
|
||||
def set_name(self, name):
|
||||
def set_name(self, name: str) -> bool:
|
||||
""" set name of this chat.
|
||||
|
||||
:param name: as a unicode string.
|
||||
:returns: None
|
||||
:returns: True on success, False otherwise
|
||||
"""
|
||||
name = as_dc_charpointer(name)
|
||||
return lib.dc_set_chat_name(self.account._dc_context, self.id, name)
|
||||
return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name))
|
||||
|
||||
def mute(self, duration=None):
|
||||
def mute(self, duration: Optional[int] = None) -> None:
|
||||
""" mutes the chat
|
||||
|
||||
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
|
||||
@@ -135,7 +136,7 @@ class Chat(object):
|
||||
if not bool(ret):
|
||||
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
||||
|
||||
def unmute(self):
|
||||
def unmute(self) -> None:
|
||||
""" unmutes the chat
|
||||
|
||||
:returns: None
|
||||
@@ -144,7 +145,7 @@ class Chat(object):
|
||||
if not bool(ret):
|
||||
raise ValueError("Failed to unmute chat")
|
||||
|
||||
def get_mute_duration(self):
|
||||
def get_mute_duration(self) -> int:
|
||||
""" Returns the number of seconds until the mute of this chat is lifted.
|
||||
|
||||
:param duration:
|
||||
@@ -152,37 +153,37 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
|
||||
|
||||
def get_ephemeral_timer(self):
|
||||
def get_ephemeral_timer(self) -> int:
|
||||
""" get ephemeral timer.
|
||||
|
||||
:returns: ephemeral timer value in seconds
|
||||
"""
|
||||
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
|
||||
|
||||
def set_ephemeral_timer(self, timer):
|
||||
def set_ephemeral_timer(self, timer: int) -> bool:
|
||||
""" set ephemeral timer.
|
||||
|
||||
:param: timer value in seconds
|
||||
|
||||
:returns: None
|
||||
:returns: True on success, False otherwise
|
||||
"""
|
||||
return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)
|
||||
return bool(lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer))
|
||||
|
||||
def get_type(self):
|
||||
def get_type(self) -> int:
|
||||
""" (deprecated) return type of this chat.
|
||||
|
||||
:returns: one of const.DC_CHAT_TYPE_*
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat)
|
||||
|
||||
def get_encryption_info(self):
|
||||
def get_encryption_info(self) -> Optional[str]:
|
||||
"""Return encryption info for this chat.
|
||||
|
||||
:returns: a string with encryption preferences of all chat members"""
|
||||
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_join_qr(self):
|
||||
def get_join_qr(self) -> Optional[str]:
|
||||
""" get/create Join-Group QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
@@ -194,7 +195,7 @@ class Chat(object):
|
||||
|
||||
# ------ chat messaging API ------------------------------
|
||||
|
||||
def send_msg(self, msg):
|
||||
def send_msg(self, msg: Message) -> Message:
|
||||
"""send a message by using a ready Message object.
|
||||
|
||||
:param msg: a :class:`deltachat.message.Message` instance
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
from typing import Any, List
|
||||
|
||||
from .capi import lib
|
||||
|
||||
|
||||
for name in dir(lib):
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name.startswith("DC_"):
|
||||
globals()[name] = getattr(lib, name)
|
||||
del name
|
||||
return getattr(lib, name)
|
||||
return globals()[name]
|
||||
|
||||
|
||||
def __dir__() -> List[str]:
|
||||
return sorted(name for name in dir(lib) if name.startswith("DC_"))
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
""" Contact object. """
|
||||
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from . import const, props
|
||||
from .capi import ffi, lib
|
||||
from .chat import Chat
|
||||
from . import const
|
||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer
|
||||
|
||||
|
||||
class Contact(object):
|
||||
@@ -35,18 +37,25 @@ class Contact(object):
|
||||
)
|
||||
|
||||
@props.with_doc
|
||||
def addr(self):
|
||||
def addr(self) -> str:
|
||||
""" normalized e-mail address for this account. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||
|
||||
@props.with_doc
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
""" display name for this contact. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||
|
||||
# deprecated alias
|
||||
display_name = name
|
||||
|
||||
@props.with_doc
|
||||
def last_seen(self) -> date:
|
||||
"""Last seen timestamp."""
|
||||
return datetime.fromtimestamp(
|
||||
lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc
|
||||
)
|
||||
|
||||
def is_blocked(self):
|
||||
""" Return True if the contact is blocked. """
|
||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||
@@ -67,15 +76,13 @@ class Contact(object):
|
||||
""" Return True if the contact is verified. """
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
|
||||
def get_profile_image(self):
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
"""Get contact profile image.
|
||||
|
||||
:returns: path to profile image, None if no profile image exists.
|
||||
"""
|
||||
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
|
||||
if dc_res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
return from_optional_dc_charpointer(dc_res)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from .capi import lib
|
||||
from .capi import ffi
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, TypeVar, Generator, Callable
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def as_dc_charpointer(obj):
|
||||
@@ -11,21 +14,28 @@ def as_dc_charpointer(obj):
|
||||
return obj
|
||||
|
||||
|
||||
def iter_array(dc_array_t, constructor):
|
||||
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
|
||||
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
|
||||
yield constructor(lib.dc_array_get_id(dc_array_t, i))
|
||||
|
||||
|
||||
def from_dc_charpointer(obj):
|
||||
def from_dc_charpointer(obj) -> str:
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
raise ValueError
|
||||
|
||||
|
||||
def from_optional_dc_charpointer(obj) -> Optional[str]:
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
return None
|
||||
|
||||
|
||||
class DCLot:
|
||||
def __init__(self, dc_lot):
|
||||
def __init__(self, dc_lot) -> None:
|
||||
self._dc_lot = dc_lot
|
||||
|
||||
def id(self):
|
||||
def id(self) -> int:
|
||||
return lib.dc_lot_get_id(self._dc_lot)
|
||||
|
||||
def state(self):
|
||||
|
||||
@@ -11,7 +11,7 @@ from imapclient import IMAPClient
|
||||
from imapclient.exceptions import IMAPClientError
|
||||
import imaplib
|
||||
import deltachat
|
||||
from deltachat import const
|
||||
from deltachat import const, Account
|
||||
|
||||
|
||||
SEEN = b'\\Seen'
|
||||
@@ -62,7 +62,7 @@ def dc_account_after_shutdown(account):
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account):
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
|
||||
@@ -9,11 +9,11 @@ from .hookspec import account_hookimpl
|
||||
from contextlib import contextmanager
|
||||
from .capi import ffi, lib
|
||||
from .message import map_system_message
|
||||
from .cutil import from_dc_charpointer
|
||||
from .cutil import from_optional_dc_charpointer
|
||||
|
||||
|
||||
class FFIEvent:
|
||||
def __init__(self, name, data1, data2):
|
||||
def __init__(self, name: str, data1, data2):
|
||||
self.name = name
|
||||
self.data1 = data1
|
||||
self.data2 = data2
|
||||
@@ -29,18 +29,18 @@ class FFIEventLogger:
|
||||
# to prevent garbled logging
|
||||
_loglock = threading.RLock()
|
||||
|
||||
def __init__(self, account):
|
||||
def __init__(self, account) -> None:
|
||||
self.account = account
|
||||
self.logid = self.account.get_config("displayname")
|
||||
self.init_time = time.time()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
def ac_process_ffi_event(self, ffi_event: FFIEvent) -> None:
|
||||
self.account.log(str(ffi_event))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_log_line(self, message):
|
||||
t = threading.currentThread()
|
||||
t = threading.current_thread()
|
||||
tname = getattr(t, "name", t)
|
||||
if tname == "MainThread":
|
||||
tname = "MAIN"
|
||||
@@ -69,7 +69,7 @@ class FFIEventTracker:
|
||||
self._event_queue = Queue()
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event):
|
||||
def ac_process_ffi_event(self, ffi_event: FFIEvent):
|
||||
self._event_queue.put(ffi_event)
|
||||
|
||||
def set_timeout(self, timeout):
|
||||
@@ -96,7 +96,7 @@ class FFIEventTracker:
|
||||
if rex.match(ev.name):
|
||||
return ev
|
||||
|
||||
def get_info_contains(self, regex):
|
||||
def get_info_contains(self, regex: str) -> FFIEvent:
|
||||
rex = re.compile(regex)
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
@@ -176,6 +176,7 @@ class FFIEventTracker:
|
||||
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data2 > 0:
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
return None
|
||||
|
||||
def wait_msg_delivered(self, msg):
|
||||
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
@@ -189,10 +190,10 @@ class EventThread(threading.Thread):
|
||||
|
||||
With each Account init this callback thread is started.
|
||||
"""
|
||||
def __init__(self, account):
|
||||
def __init__(self, account) -> None:
|
||||
self.account = account
|
||||
super(EventThread, self).__init__(name="events")
|
||||
self.setDaemon(True)
|
||||
self.daemon = True
|
||||
self._marked_for_shutdown = False
|
||||
self.start()
|
||||
|
||||
@@ -202,17 +203,17 @@ class EventThread(threading.Thread):
|
||||
yield
|
||||
self.account.log(message + " FINISHED")
|
||||
|
||||
def mark_shutdown(self):
|
||||
def mark_shutdown(self) -> None:
|
||||
self._marked_for_shutdown = True
|
||||
|
||||
def wait(self, timeout=None):
|
||||
def wait(self, timeout=None) -> None:
|
||||
if self == threading.current_thread():
|
||||
# we are in the callback thread and thus cannot
|
||||
# wait for the thread-loop to finish.
|
||||
return
|
||||
self.join(timeout=timeout)
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
""" get and run events until shutdown. """
|
||||
with self.log_execution("EVENT THREAD"):
|
||||
self._inner_run()
|
||||
@@ -234,7 +235,7 @@ class EventThread(threading.Thread):
|
||||
# function which provides us signature info of an event call
|
||||
evt_name = deltachat.get_dc_event_name(evt)
|
||||
if lib.dc_event_has_string_data(evt):
|
||||
data2 = from_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
else:
|
||||
data2 = lib.dc_event_get_data2_int(event)
|
||||
|
||||
@@ -250,7 +251,7 @@ class EventThread(threading.Thread):
|
||||
if self.account._dc_context is not None:
|
||||
raise
|
||||
|
||||
def _map_ffi_event(self, ffi_event):
|
||||
def _map_ffi_event(self, ffi_event: FFIEvent):
|
||||
name = ffi_event.name
|
||||
account = self.account
|
||||
if name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import os
|
||||
import re
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer, as_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Message(object):
|
||||
@@ -75,7 +76,7 @@ class Message(object):
|
||||
return lib.dc_msg_get_id(self._dc_msg)
|
||||
|
||||
@props.with_doc
|
||||
def text(self):
|
||||
def text(self) -> str:
|
||||
"""unicode text of this messages (might be empty if not a text message). """
|
||||
return from_dc_charpointer(lib.dc_msg_get_text(self._dc_msg))
|
||||
|
||||
@@ -84,9 +85,9 @@ class Message(object):
|
||||
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
|
||||
|
||||
@props.with_doc
|
||||
def html(self):
|
||||
def html(self) -> str:
|
||||
"""html text of this messages (might be empty if not an html message). """
|
||||
return from_dc_charpointer(
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
|
||||
|
||||
def has_html(self):
|
||||
@@ -113,12 +114,13 @@ class Message(object):
|
||||
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
|
||||
|
||||
@props.with_doc
|
||||
def basename(self):
|
||||
def basename(self) -> str:
|
||||
"""basename of the attachment if it exists, otherwise empty string. """
|
||||
# FIXME, it does not return basename
|
||||
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
||||
|
||||
@props.with_doc
|
||||
def filemime(self):
|
||||
def filemime(self) -> str:
|
||||
"""mime type of the file (if it exists)"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||
|
||||
@@ -130,7 +132,7 @@ class Message(object):
|
||||
""" return True if this message is a setup message. """
|
||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||
|
||||
def get_setupcodebegin(self):
|
||||
def get_setupcodebegin(self) -> str:
|
||||
""" return the first characters of a setup code in a setup message. """
|
||||
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
|
||||
|
||||
@@ -146,7 +148,7 @@ class Message(object):
|
||||
""" return True if this message was forwarded. """
|
||||
return bool(lib.dc_msg_is_forwarded(self._dc_msg))
|
||||
|
||||
def get_message_info(self):
|
||||
def get_message_info(self) -> str:
|
||||
""" Return informational text for a single message.
|
||||
|
||||
The text is multiline and may contain eg. the raw text of the message.
|
||||
@@ -203,11 +205,11 @@ class Message(object):
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
@property
|
||||
def quoted_text(self):
|
||||
def quoted_text(self) -> Optional[str]:
|
||||
"""Text inside the quote
|
||||
|
||||
:returns: Quoted text"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
|
||||
@property
|
||||
def quote(self):
|
||||
@@ -223,6 +225,10 @@ class Message(object):
|
||||
"""Quote setter"""
|
||||
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
|
||||
|
||||
def force_plaintext(self) -> None:
|
||||
"""Force the message to be sent in plain text."""
|
||||
lib.dc_msg_force_plaintext(self._dc_msg)
|
||||
|
||||
def get_mime_headers(self):
|
||||
""" return mime-header object for an incoming message.
|
||||
|
||||
@@ -240,9 +246,9 @@ class Message(object):
|
||||
return email.message_from_string(s)
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
def error(self) -> Optional[str]:
|
||||
"""Error message"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
|
||||
@property
|
||||
def chat(self):
|
||||
@@ -255,12 +261,12 @@ class Message(object):
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
@props.with_doc
|
||||
def override_sender_name(self):
|
||||
def override_sender_name(self) -> Optional[str]:
|
||||
"""the name that should be shown over the message instead of the contact display name.
|
||||
|
||||
Usually used to impersonate someone else.
|
||||
"""
|
||||
return from_dc_charpointer(
|
||||
return from_optional_dc_charpointer(
|
||||
lib.dc_msg_get_override_sender_name(self._dc_msg))
|
||||
|
||||
def set_override_sender_name(self, name):
|
||||
|
||||
@@ -14,7 +14,7 @@ class Provider(object):
|
||||
:param domain: The email to get the provider info for.
|
||||
"""
|
||||
|
||||
def __init__(self, account, addr):
|
||||
def __init__(self, account, addr) -> None:
|
||||
provider = ffi.gc(
|
||||
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
|
||||
lib.dc_provider_unref,
|
||||
@@ -24,19 +24,19 @@ class Provider(object):
|
||||
self._provider = provider
|
||||
|
||||
@property
|
||||
def overview_page(self):
|
||||
def overview_page(self) -> str:
|
||||
"""URL to the overview page of the provider on providers.delta.chat."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_overview_page(self._provider))
|
||||
|
||||
@property
|
||||
def get_before_login_hints(self):
|
||||
def get_before_login_hints(self) -> str:
|
||||
"""Should be shown to the user on login."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_before_login_hints(self._provider))
|
||||
lib.dc_provider_get_before_login_hint(self._provider))
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
def status(self) -> int:
|
||||
"""The status of the provider information.
|
||||
|
||||
This is one of the
|
||||
|
||||
@@ -9,6 +9,7 @@ import fnmatch
|
||||
import time
|
||||
import weakref
|
||||
import tempfile
|
||||
from typing import List, Dict, Callable
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
@@ -126,7 +127,7 @@ def pytest_report_header(config, startdir):
|
||||
|
||||
|
||||
class SessionLiveConfigFromFile:
|
||||
def __init__(self, fn):
|
||||
def __init__(self, fn) -> None:
|
||||
self.fn = fn
|
||||
self.configlist = []
|
||||
for line in open(fn):
|
||||
@@ -137,19 +138,21 @@ class SessionLiveConfigFromFile:
|
||||
d[name] = value
|
||||
self.configlist.append(d)
|
||||
|
||||
def get(self, index):
|
||||
def get(self, index: int):
|
||||
return self.configlist[index]
|
||||
|
||||
def exists(self):
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
class SessionLiveConfigFromURL:
|
||||
def __init__(self, url):
|
||||
configlist: List[Dict[str, str]]
|
||||
|
||||
def __init__(self, url: str) -> None:
|
||||
self.configlist = []
|
||||
self.url = url
|
||||
|
||||
def get(self, index):
|
||||
def get(self, index: int):
|
||||
try:
|
||||
return self.configlist[index]
|
||||
except IndexError:
|
||||
@@ -162,7 +165,7 @@ class SessionLiveConfigFromURL:
|
||||
self.configlist.append(config)
|
||||
return config
|
||||
|
||||
def exists(self):
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
@@ -179,7 +182,7 @@ def session_liveconfig(request):
|
||||
@pytest.fixture
|
||||
def data(request):
|
||||
class Data:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
# trying to find test data heuristically
|
||||
# because we are run from a dev-setup with pytest direct,
|
||||
# through tox, and then maybe also from deltachat-binding
|
||||
@@ -210,7 +213,10 @@ def data(request):
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
class AccountMaker:
|
||||
def __init__(self):
|
||||
_finalizers: List[Callable[[], None]]
|
||||
_accounts: List[Account]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.live_count = 0
|
||||
self.offline_count = 0
|
||||
self._finalizers = []
|
||||
@@ -297,21 +303,20 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
|
||||
def get_online_configuring_account(self, sentbox=False, move=False,
|
||||
pre_generated_key=True, quiet=False, config={}):
|
||||
ac, configdict = self.get_online_config(
|
||||
pre_generated_key=pre_generated_key, quiet=quiet)
|
||||
configdict.update(config)
|
||||
configdict["mvbox_watch"] = str(int(mvbox))
|
||||
configdict["mvbox_move"] = str(int(move))
|
||||
configdict["sentbox_watch"] = str(int(sentbox))
|
||||
ac.update_config(configdict)
|
||||
ac._configtracker = ac.configure()
|
||||
return ac
|
||||
|
||||
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
|
||||
def get_one_online_account(self, pre_generated_key=True, move=False):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
|
||||
pre_generated_key=pre_generated_key, move=move)
|
||||
self.wait_configure_and_start_io([ac1])
|
||||
return ac1
|
||||
|
||||
@@ -330,7 +335,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
return accounts
|
||||
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
""" Clones addr, mail_pw, mvbox_watch, mvbox_move, sentbox_watch and the
|
||||
""" Clones addr, mail_pw, mvbox_move, sentbox_watch and the
|
||||
direct_imap object of an online account. This simulates the user setting
|
||||
up a new device without importing a backup.
|
||||
|
||||
@@ -345,7 +350,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
ac.update_config(dict(
|
||||
addr=account.get_config("addr"),
|
||||
mail_pw=account.get_config("mail_pw"),
|
||||
mvbox_watch=account.get_config("mvbox_watch"),
|
||||
mvbox_move=account.get_config("mvbox_move"),
|
||||
sentbox_watch=account.get_config("sentbox_watch"),
|
||||
))
|
||||
@@ -423,7 +427,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
pass
|
||||
imap.dump_imap_structures(tmpdir, logfile=logfile)
|
||||
|
||||
def get_accepted_chat(self, ac1, ac2):
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account):
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
@@ -451,7 +455,9 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
|
||||
|
||||
class BotProcess:
|
||||
def __init__(self, popen, bot_cfg):
|
||||
stdout_queue: queue.Queue
|
||||
|
||||
def __init__(self, popen, bot_cfg) -> None:
|
||||
self.popen = popen
|
||||
self.addr = bot_cfg["addr"]
|
||||
|
||||
@@ -459,10 +465,10 @@ class BotProcess:
|
||||
# the (unicode) lines available for readers through a queue.
|
||||
self.stdout_queue = queue.Queue()
|
||||
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
|
||||
t.setDaemon(1)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def _run_stdout_thread(self):
|
||||
def _run_stdout_thread(self) -> None:
|
||||
try:
|
||||
while 1:
|
||||
line = self.popen.stdout.readline()
|
||||
@@ -474,10 +480,10 @@ class BotProcess:
|
||||
finally:
|
||||
self.stdout_queue.put(None)
|
||||
|
||||
def kill(self):
|
||||
def kill(self) -> None:
|
||||
self.popen.kill()
|
||||
|
||||
def wait(self, timeout=30):
|
||||
def wait(self, timeout=30) -> None:
|
||||
self.popen.wait(timeout=timeout)
|
||||
|
||||
def fnmatch_lines(self, pattern_lines):
|
||||
@@ -509,14 +515,14 @@ def tmp_db_path(tmpdir):
|
||||
@pytest.fixture
|
||||
def lp():
|
||||
class Printer:
|
||||
def sec(self, msg):
|
||||
def sec(self, msg: str) -> None:
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg):
|
||||
def step(self, msg: str) -> None:
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
|
||||
def indent(self, msg):
|
||||
def indent(self, msg: str) -> None:
|
||||
print(" " + msg)
|
||||
|
||||
return Printer()
|
||||
|
||||
@@ -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")
|
||||
@@ -57,7 +57,7 @@ class TestOfflineAccountBasic:
|
||||
alice_public = data.read_path("key/alice-public.asc")
|
||||
alice_secret = data.read_path("key/alice-secret.asc")
|
||||
assert alice_public and alice_secret
|
||||
ac._preconfigure_keypair("alice@example.com", alice_public, alice_secret)
|
||||
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -265,23 +265,23 @@ class TestOfflineChat:
|
||||
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
|
||||
|
||||
def test_group_chat_creation_with_translation(self, ac1):
|
||||
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %1$s")
|
||||
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "abc %1$s xyz %2$s")
|
||||
ac1._evtracker.consume_events()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %2$s")
|
||||
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(500, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
|
||||
assert chat.get_name() == "title1"
|
||||
assert contact1 in chat.get_contacts()
|
||||
assert contact2 in chat.get_contacts()
|
||||
assert not chat.is_promoted()
|
||||
msg = chat.get_draft()
|
||||
assert msg.text == "xyz title1"
|
||||
chat = ac1.create_group_chat(name="homework", contacts=[])
|
||||
assert chat.get_name() == "homework"
|
||||
chat.send_text("Now we have a group for homework")
|
||||
assert chat.is_promoted()
|
||||
chat.set_name("Homework")
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
@@ -737,7 +737,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 +752,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
|
||||
@@ -860,7 +858,7 @@ class TestOnlineAccount:
|
||||
|
||||
def test_mvbox_sentbox_threads(self, acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True, sentbox=True)
|
||||
ac1 = acfactory.get_online_configuring_account(move=True, sentbox=True)
|
||||
|
||||
lp.sec("ac2: start without mvbox/sentbox threads")
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
@@ -874,16 +872,20 @@ class TestOnlineAccount:
|
||||
|
||||
def test_move_works(self, acfactory):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(mvbox=True, move=True)
|
||||
ac2 = acfactory.get_online_configuring_account(move=True)
|
||||
acfactory.wait_configure_and_start_io()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message1")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
|
||||
# Message is moved to the movebox
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
# Message is downloaded
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
|
||||
def test_move_works_on_self_sent(self, acfactory):
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True)
|
||||
ac1 = acfactory.get_online_configuring_account(move=True)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
acfactory.wait_configure_and_start_io()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
@@ -953,7 +955,7 @@ class TestOnlineAccount:
|
||||
assert msg_in.is_forwarded()
|
||||
|
||||
def test_send_self_message(self, acfactory, lp):
|
||||
ac1 = acfactory.get_one_online_account(mvbox=True, move=True)
|
||||
ac1 = acfactory.get_one_online_account(move=True)
|
||||
lp.sec("ac1: create self chat")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
chat.send_text("hello")
|
||||
@@ -983,7 +985,8 @@ class TestOnlineAccount:
|
||||
assert msg2 in chat2.get_messages()
|
||||
assert chat2.is_contact_request()
|
||||
assert chat2.count_fresh_messages() == 1
|
||||
assert msg2.time_received >= msg1.time_sent
|
||||
# Like it or not, this assert is flaky
|
||||
# assert msg2.time_received >= msg1.time_sent
|
||||
|
||||
lp.sec("create new chat with contact and verify it's proper")
|
||||
chat2b = msg2.create_chat()
|
||||
@@ -1034,31 +1037,70 @@ class TestOnlineAccount:
|
||||
|
||||
def test_moved_markseen(self, acfactory, lp):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True, config={"inbox_watch": "0"})
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(move=True)
|
||||
acfactory.wait_configure_and_start_io([ac1, ac2])
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
ac1.direct_imap.idle_start()
|
||||
ac2.stop_io()
|
||||
ac2.direct_imap.idle_start()
|
||||
|
||||
ac1.create_chat(ac2).send_text("Hello!")
|
||||
ac1.direct_imap.idle_check(terminate=True)
|
||||
ac1.stop_io()
|
||||
|
||||
# Wait for the message to arrive.
|
||||
ac2.direct_imap.idle_check(terminate=True)
|
||||
|
||||
# Emulate moving of the message to DeltaChat folder by Sieve rule.
|
||||
# mailcow server contains this rule by default.
|
||||
ac1.direct_imap.conn.move(["*"], "DeltaChat")
|
||||
ac2.direct_imap.conn.move(["*"], "DeltaChat")
|
||||
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
ac1.direct_imap.idle_start()
|
||||
ac1.start_io()
|
||||
ac1.direct_imap.idle_wait_for_seen()
|
||||
ac1.direct_imap.idle_done()
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
ac2.direct_imap.idle_start()
|
||||
ac2.start_io()
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
fetch = list(ac1.direct_imap.conn.fetch("*", b'FLAGS').values())
|
||||
# Accept the contact request.
|
||||
msg.chat.accept()
|
||||
ac2.mark_seen_messages([msg])
|
||||
ac2.direct_imap.idle_wait_for_seen()
|
||||
ac2.direct_imap.idle_done()
|
||||
|
||||
fetch = list(ac2.direct_imap.conn.fetch("*", b'FLAGS').values())
|
||||
flags = fetch[-1][b'FLAGS']
|
||||
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
|
||||
|
||||
def test_message_override_sender_name(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -1095,8 +1137,8 @@ class TestOnlineAccount:
|
||||
def test_markseen_message_and_mdn(self, acfactory, mvbox_move):
|
||||
# Please only change this test if you are very sure that it will still catch the issues it catches now.
|
||||
# We had so many problems with markseen, if in doubt, rather create another test, it can't harm.
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
|
||||
ac2 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
ac2 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
|
||||
acfactory.wait_configure_and_start_io()
|
||||
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||
@@ -1142,7 +1184,7 @@ class TestOnlineAccount:
|
||||
assert not msg_reply1.chat.is_group()
|
||||
assert msg_reply1.chat.id == private_chat1.id
|
||||
|
||||
def test_mdn_asymetric(self, acfactory, lp):
|
||||
def test_mdn_asymmetric(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts(move=True)
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
@@ -1166,6 +1208,9 @@ class TestOnlineAccount:
|
||||
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
ac1.direct_imap.select_config_folder("mvbox")
|
||||
ac1.direct_imap.idle_start()
|
||||
|
||||
lp.sec("ac2: mark incoming message as seen")
|
||||
ac2.mark_seen_messages([msg])
|
||||
|
||||
@@ -1175,6 +1220,9 @@ class TestOnlineAccount:
|
||||
|
||||
assert len(chat.get_messages()) == 1
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
assert ac1.direct_imap.idle_wait_for_seen()
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
assert msg_out.is_out_mdn_received()
|
||||
|
||||
@@ -1359,7 +1407,7 @@ class TestOnlineAccount:
|
||||
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac1.set_config("show_emails", "2")
|
||||
ac1.create_contact("alice@example.com").create_chat()
|
||||
ac1.create_contact("alice@example.org").create_chat()
|
||||
|
||||
acfactory.wait_configure(ac1)
|
||||
ac1.direct_imap.create_folder("Drafts")
|
||||
@@ -1373,7 +1421,7 @@ class TestOnlineAccount:
|
||||
ac1.direct_imap.append("Drafts", """
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.com
|
||||
To: alice@example.org
|
||||
Message-ID: <aepiors@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
@@ -1382,7 +1430,7 @@ class TestOnlineAccount:
|
||||
ac1.direct_imap.append("Sent", """
|
||||
From: ac1 <{}>
|
||||
Subject: subj
|
||||
To: alice@example.com
|
||||
To: alice@example.org
|
||||
Message-ID: <hsabaeni@example.org>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
@@ -1413,6 +1461,35 @@ class TestOnlineAccount:
|
||||
assert msg2.text == "subj – message in Drafts that is moved to Sent later"
|
||||
assert len(msg.chat.get_messages()) == 2
|
||||
|
||||
def test_no_old_msg_is_fresh(self, acfactory, lp):
|
||||
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("e2ee_enabled", "0")
|
||||
ac1_clone.set_config("e2ee_enabled", "0")
|
||||
ac2.set_config("e2ee_enabled", "0")
|
||||
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1.create_chat(ac2)
|
||||
ac1_clone.create_chat(ac2)
|
||||
|
||||
lp.sec("Send a first message from ac2 to ac1 and check that it's 'fresh'")
|
||||
first_msg_id = ac2.create_chat(ac1).send_text("Hi")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 1
|
||||
assert len(list(ac1.get_fresh_messages())) == 1
|
||||
|
||||
lp.sec("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||
ac1_clone.create_chat(ac2).send_text("Hi back")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
|
||||
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
|
||||
|
||||
def test_prefer_encrypt(self, acfactory, lp):
|
||||
"""Test quorum rule for encryption preference in 1:1 and group chat."""
|
||||
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
|
||||
@@ -2077,10 +2154,8 @@ class TestOnlineAccount:
|
||||
assert msg_back.chat == chat
|
||||
assert chat.get_profile_image() is None
|
||||
|
||||
@pytest.mark.parametrize("inbox_watch", ["0", "1"])
|
||||
def test_connectivity(self, acfactory, lp, inbox_watch):
|
||||
def test_connectivity(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1.set_config("inbox_watch", inbox_watch)
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
|
||||
@@ -2320,7 +2395,7 @@ class TestOnlineAccount:
|
||||
|
||||
def test_immediate_autodelete(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account(mvbox=False, move=False, sentbox=False)
|
||||
ac2 = acfactory.get_online_configuring_account(move=False, sentbox=False)
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
@@ -2581,31 +2656,26 @@ class TestOnlineAccount:
|
||||
assert received_reply.quoted_text == "hello"
|
||||
assert received_reply.quote.id == out_msg.id
|
||||
|
||||
@pytest.mark.parametrize("folder,move,expected_destination,inbox_watch,", [
|
||||
("xyz", False, "xyz", "1"), # Test that emails are recognized in a random folder but not moved
|
||||
("xyz", True, "DeltaChat", "1"), # ...emails are found in a random folder and moved to DeltaChat
|
||||
("Spam", False, "INBOX", "1"), # ...emails are moved from the spam folder to the Inbox
|
||||
("INBOX", False, "INBOX", "0"), # ...emails are found in the `Inbox` folder even if `inbox_watch` is "0"
|
||||
@pytest.mark.parametrize("folder,move,expected_destination,", [
|
||||
("xyz", False, "xyz"), # Test that emails are recognized in a random folder but not moved
|
||||
("xyz", True, "DeltaChat"), # ...emails are found in a random folder and moved to DeltaChat
|
||||
("Spam", False, "INBOX"), # ...emails are moved from the spam folder to the Inbox
|
||||
])
|
||||
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
|
||||
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
|
||||
def test_scan_folders(self, acfactory, lp, folder, move, expected_destination, inbox_watch):
|
||||
def test_scan_folders(self, acfactory, lp, folder, move, expected_destination):
|
||||
"""Delta Chat periodically scans all folders for new messages to make sure we don't miss any."""
|
||||
variant = folder + "-" + str(move) + "-" + expected_destination
|
||||
lp.sec("Testing variant " + variant)
|
||||
ac1 = acfactory.get_online_configuring_account(move=move)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
ac1.set_config("inbox_watch", inbox_watch)
|
||||
|
||||
acfactory.wait_configure(ac1)
|
||||
ac1.direct_imap.create_folder(folder)
|
||||
|
||||
acfactory.wait_configure_and_start_io()
|
||||
# Wait until each folder was selected once and we are IDLEing:
|
||||
if inbox_watch == "1":
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
else:
|
||||
ac1._evtracker.get_info_contains("IMAP-fake-IDLE: no folder, waiting for interrupt")
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
ac1.stop_io()
|
||||
|
||||
# Send a message to ac1 and move it to the mvbox:
|
||||
@@ -2621,11 +2691,7 @@ class TestOnlineAccount:
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
# Wait until the message was moved (if at all) and we are IDLEing again:
|
||||
if inbox_watch == "1":
|
||||
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
|
||||
else:
|
||||
ac1._evtracker.get_info_contains("IMAP-fake-IDLE: no folder, waiting for interrupt")
|
||||
# The message has been downloaded, which means it has reached its destination.
|
||||
ac1.direct_imap.select_folder(expected_destination)
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 1
|
||||
if folder != expected_destination:
|
||||
@@ -2648,7 +2714,7 @@ class TestOnlineAccount:
|
||||
if mvbox_move:
|
||||
assert ac.get_config("configured_mvbox_folder")
|
||||
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=mvbox_move, move=mvbox_move)
|
||||
ac1 = acfactory.get_online_configuring_account(move=mvbox_move)
|
||||
ac1.set_config("sentbox_move", "1")
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
|
||||
@@ -2748,7 +2814,7 @@ class TestOnlineAccount:
|
||||
|
||||
def test_delete_deltachat_folder(self, acfactory):
|
||||
"""Test that DeltaChat folder is recreated if user deletes it manually."""
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True)
|
||||
ac1 = acfactory.get_online_configuring_account(move=True)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
acfactory.wait_configure(ac1)
|
||||
|
||||
@@ -2876,7 +2942,8 @@ class TestGroupStressTests:
|
||||
lp.sec("ac2: check that ac3 is removed")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert msg.chat.num_contacts() == chat.num_contacts()
|
||||
assert chat.num_contacts() == 2
|
||||
assert msg.chat.num_contacts() == 2
|
||||
acfactory.dump_imap_summary(sys.stdout)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
[tox]
|
||||
isolated_build = true
|
||||
envlist =
|
||||
py3
|
||||
lint
|
||||
mypy
|
||||
auditwheels
|
||||
|
||||
[testenv]
|
||||
@@ -42,6 +44,15 @@ commands =
|
||||
flake8 tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
deps =
|
||||
mypy
|
||||
typing
|
||||
types-setuptools
|
||||
types-requests
|
||||
commands =
|
||||
mypy --no-incremental src/
|
||||
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.6 environment as the base environment
|
||||
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
|
||||
@@ -19,11 +19,9 @@ export DCC_RS_TARGET=release
|
||||
|
||||
# Configure access to a base python and to several python interpreters
|
||||
# needed by tox below.
|
||||
export PATH=$PATH:/opt/python/cp36-cp36m/bin
|
||||
export PATH=$PATH:/opt/python/cp37-cp37m/bin
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
pushd /bin
|
||||
rm -f python3.6
|
||||
ln -s /opt/python/cp36-cp36m/bin/python3.6
|
||||
rm -f python3.7
|
||||
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
||||
rm -f python3.8
|
||||
|
||||
144
src/accounts.rs
144
src/accounts.rs
@@ -29,21 +29,21 @@ pub struct Accounts {
|
||||
|
||||
impl Accounts {
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
pub async fn new(os_name: String, dir: PathBuf) -> Result<Self> {
|
||||
pub async fn new(dir: PathBuf) -> Result<Self> {
|
||||
if !dir.exists().await {
|
||||
Accounts::create(os_name, &dir).await?;
|
||||
Accounts::create(&dir).await?;
|
||||
}
|
||||
|
||||
Accounts::open(dir).await
|
||||
}
|
||||
|
||||
/// Creates a new default structure.
|
||||
pub async fn create(os_name: String, dir: &PathBuf) -> Result<()> {
|
||||
pub async fn create(dir: &PathBuf) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
|
||||
Config::new(os_name.clone(), dir).await?;
|
||||
Config::new(dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -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,12 +111,24 @@ 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 os_name = self.config.os_name().await;
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
|
||||
let ctx = Context::new(os_name, account_config.dbfile().into(), account_config.id).await?;
|
||||
let ctx = Context::new(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)
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
@@ -183,13 +202,7 @@ impl Accounts {
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
let ctx = Context::with_blobdir(
|
||||
self.config.os_name().await,
|
||||
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)
|
||||
@@ -349,7 +362,6 @@ pub struct Config {
|
||||
/// This is serialized into TOML.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct InnerConfig {
|
||||
pub os_name: String,
|
||||
/// The currently selected account.
|
||||
pub selected_account: u32,
|
||||
pub next_id: u32,
|
||||
@@ -357,9 +369,8 @@ struct InnerConfig {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn new(os_name: String, dir: &PathBuf) -> Result<Self> {
|
||||
pub async fn new(dir: &PathBuf) -> Result<Self> {
|
||||
let inner = InnerConfig {
|
||||
os_name,
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
@@ -374,10 +385,6 @@ impl Config {
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub async fn os_name(&self) -> String {
|
||||
self.inner.os_name.clone()
|
||||
}
|
||||
|
||||
/// Sync the inmemory representation to disk.
|
||||
async fn sync(&self) -> Result<()> {
|
||||
fs::write(&self.file, toml::to_string_pretty(&self.inner)?)
|
||||
@@ -396,12 +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(
|
||||
self.inner.os_name.clone(),
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -426,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)
|
||||
}
|
||||
|
||||
@@ -498,7 +513,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts1").into();
|
||||
|
||||
let mut accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts1 = Accounts::new(p.clone()).await.unwrap();
|
||||
accounts1.add_account().await.unwrap();
|
||||
|
||||
let accounts2 = Accounts::open(p).await.unwrap();
|
||||
@@ -516,7 +531,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
@@ -543,7 +558,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
@@ -564,14 +579,12 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let extern_dbfile: PathBuf = dir.path().join("other").into();
|
||||
let ctx = Context::new("my_os".into(), extern_dbfile.clone(), 0)
|
||||
.await
|
||||
.unwrap();
|
||||
let ctx = Context::new(extern_dbfile.clone(), 0).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -601,7 +614,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 1..10 {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
@@ -621,7 +634,7 @@ mod tests {
|
||||
let dummy_accounts = 10;
|
||||
|
||||
let (id0, id1, id2) = {
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
accounts.add_account().await?;
|
||||
let ids = accounts.get_all().await;
|
||||
assert_eq!(ids.len(), 1);
|
||||
@@ -656,7 +669,7 @@ mod tests {
|
||||
assert!(id2 > id1 + dummy_accounts);
|
||||
|
||||
let (id0_reopened, id1_reopened, id2_reopened) = {
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
@@ -701,7 +714,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
|
||||
// Make sure there are no accounts.
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
@@ -721,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
|
||||
|
||||
59
src/blob.rs
59
src/blob.rs
@@ -24,6 +24,7 @@ use crate::constants::{
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::message;
|
||||
|
||||
/// Represents a file in the blob directory.
|
||||
@@ -63,7 +64,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 +88,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 +107,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 +149,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 {
|
||||
{
|
||||
@@ -621,10 +621,13 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::{
|
||||
chat,
|
||||
message::Message,
|
||||
test_utils::{self, TestContext},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use image::Pixel;
|
||||
|
||||
#[async_std::test]
|
||||
@@ -1066,4 +1069,38 @@ mod tests {
|
||||
assert_eq!(img.height() as u32, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_increation_in_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
|
||||
let file = t.get_blobdir().join("anyfile.dat");
|
||||
File::create(&file).await?.write_all("bla".as_ref()).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let prepared_id = chat::prepare_msg(&t, chat_id, &mut msg).await?;
|
||||
assert_eq!(prepared_id, msg.id);
|
||||
assert!(msg.is_increation());
|
||||
|
||||
let msg = Message::load_from_db(&t, prepared_id).await?;
|
||||
assert!(msg.is_increation());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_increation_not_blobdir() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
|
||||
assert_ne!(t.get_blobdir().to_str(), t.dir.path().to_str());
|
||||
|
||||
let file = t.dir.path().join("anyfile.dat");
|
||||
File::create(&file).await?.write_all("bla".as_ref()).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
assert!(chat::prepare_msg(&t, chat_id, &mut msg).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
1084
src/chat.rs
1084
src/chat.rs
File diff suppressed because it is too large
Load Diff
@@ -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::{
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -395,16 +396,26 @@ 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
|
||||
// chat timestamps are only exact to the second and sorting by timestamp
|
||||
// would not work.
|
||||
// Message timestamps are "smeared" and unique, so we don't have this problem
|
||||
// if we have any message (can be a draft) in all chats.
|
||||
// Instead of setting drafts for chat_id1 and chat_id3, we could also sleep
|
||||
// 2s here.
|
||||
for chat_id in &[chat_id1, chat_id3, chat_id2] {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
}
|
||||
|
||||
// drafts are sorted to the top
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
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();
|
||||
@@ -435,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());
|
||||
@@ -444,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());
|
||||
@@ -489,7 +500,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -497,7 +508,6 @@ mod tests {
|
||||
\n\
|
||||
hello foo\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -518,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?;
|
||||
@@ -551,7 +561,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg5678@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -559,7 +569,6 @@ mod tests {
|
||||
\n\
|
||||
hello foo\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -575,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?;
|
||||
@@ -586,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?;
|
||||
|
||||
@@ -35,6 +35,10 @@ pub(crate) fn str_to_color(s: &str) -> u32 {
|
||||
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
|
||||
}
|
||||
|
||||
pub fn color_int_to_hex_string(color: u32) -> String {
|
||||
format!("{:#08x}", color).replace("0x", "#")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -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,21 +65,22 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
MdnsEnabled,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
InboxWatch,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
#[strum(props(default = "0"))]
|
||||
SentboxWatch,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
MvboxWatch,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
MvboxMove,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
SentboxMove, // If `MvboxMove` is true, this config is ignored. Currently only used in tests.
|
||||
|
||||
/// 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"))]
|
||||
OnlyFetchMvbox,
|
||||
|
||||
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
|
||||
ShowEmails,
|
||||
|
||||
@@ -206,7 +205,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())),
|
||||
}
|
||||
@@ -234,6 +232,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
|
||||
@@ -290,55 +293,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(())
|
||||
}
|
||||
@@ -430,4 +407,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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ mod server_params;
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::prelude::*;
|
||||
use async_std::task;
|
||||
use itertools::Itertools;
|
||||
use job::Action;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
@@ -222,7 +221,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
if let Some(provider) = provider::get_provider_info(¶m_domain, socks5_enabled).await {
|
||||
if let Some(provider) =
|
||||
provider::get_provider_info(ctx, ¶m_domain, socks5_enabled).await
|
||||
{
|
||||
param.provider = Some(provider);
|
||||
match provider.status {
|
||||
provider::Status::Ok | provider::Status::Preparation => {
|
||||
@@ -442,8 +443,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await?
|
||||
|| ctx.get_config_bool(Config::MvboxMove).await?;
|
||||
let create_mvbox = ctx.should_watch_mvbox().await?;
|
||||
|
||||
imap.configure_folders(ctx, create_mvbox).await?;
|
||||
|
||||
@@ -454,11 +454,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 +473,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(())
|
||||
}
|
||||
|
||||
@@ -682,7 +681,11 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
|
||||
return first_err.msg.to_string();
|
||||
}
|
||||
|
||||
errors.iter().map(|e| e.to_string()).join("\n\n")
|
||||
errors
|
||||
.iter()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@@ -295,6 +295,9 @@ pub enum Viewtype {
|
||||
|
||||
/// Message is an invitation to a videochat.
|
||||
VideochatInvitation = 70,
|
||||
|
||||
/// Message is an webxdc instance.
|
||||
Webxdc = 80,
|
||||
}
|
||||
|
||||
impl Default for Viewtype {
|
||||
@@ -339,6 +342,7 @@ mod tests {
|
||||
Viewtype::VideochatInvitation,
|
||||
Viewtype::from_i32(70).unwrap()
|
||||
);
|
||||
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
129
src/contact.rs
129
src/contact.rs
@@ -5,7 +5,6 @@ use std::convert::{TryFrom, TryInto};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::path::PathBuf;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
@@ -67,6 +66,9 @@ pub struct Contact {
|
||||
/// Blocked state. Use dc_contact_is_blocked to access this field.
|
||||
pub blocked: bool,
|
||||
|
||||
/// Time when the contact was seen last time, Unix time in seconds.
|
||||
last_seen: i64,
|
||||
|
||||
/// The origin/source of the contact.
|
||||
pub origin: Origin,
|
||||
|
||||
@@ -185,7 +187,8 @@ impl Contact {
|
||||
let mut contact = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
|
||||
c.authname, c.param, c.status
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
@@ -194,15 +197,17 @@ impl Contact {
|
||||
let addr: String = row.get(1)?;
|
||||
let origin: Origin = row.get(2)?;
|
||||
let blocked: Option<bool> = row.get(3)?;
|
||||
let authname: String = row.get(4)?;
|
||||
let param: String = row.get(5)?;
|
||||
let status: Option<String> = row.get(6)?;
|
||||
let last_seen: i64 = row.get(4)?;
|
||||
let authname: String = row.get(5)?;
|
||||
let param: String = row.get(6)?;
|
||||
let status: Option<String> = row.get(7)?;
|
||||
let contact = Self {
|
||||
id: contact_id,
|
||||
name,
|
||||
authname,
|
||||
addr,
|
||||
blocked: blocked.unwrap_or_default(),
|
||||
last_seen,
|
||||
origin,
|
||||
param: param.parse().unwrap_or_default(),
|
||||
status: status.unwrap_or_default(),
|
||||
@@ -234,6 +239,11 @@ impl Contact {
|
||||
self.blocked
|
||||
}
|
||||
|
||||
/// Returns last seen timestamp.
|
||||
pub fn last_seen(&self) -> i64 {
|
||||
self.last_seen
|
||||
}
|
||||
|
||||
/// Check if a contact is blocked.
|
||||
pub async fn is_blocked_load(context: &Context, id: u32) -> Result<bool> {
|
||||
let blocked = Self::load_from_db(context, id).await?.blocked;
|
||||
@@ -301,17 +311,17 @@ impl Contact {
|
||||
/// use `dc_may_be_valid_addr()`.
|
||||
pub async fn lookup_id_by_addr(
|
||||
context: &Context,
|
||||
addr: impl AsRef<str>,
|
||||
addr: &str,
|
||||
min_origin: Origin,
|
||||
) -> Result<Option<u32>> {
|
||||
if addr.as_ref().is_empty() {
|
||||
if addr.is_empty() {
|
||||
bail!("lookup_id_by_addr: empty address");
|
||||
}
|
||||
|
||||
let addr_normalized = addr_normalize(addr.as_ref());
|
||||
let addr_normalized = addr_normalize(addr);
|
||||
|
||||
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await? {
|
||||
if addr_cmp(addr_normalized, addr_self) {
|
||||
if addr_cmp(addr_normalized, &addr_self) {
|
||||
return Ok(Some(DC_CONTACT_ID_SELF));
|
||||
}
|
||||
}
|
||||
@@ -373,7 +383,7 @@ impl Contact {
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
if addr_cmp(&addr, addr_self) {
|
||||
if addr_cmp(&addr, &addr_self) {
|
||||
return Ok((DC_CONTACT_ID_SELF, sth_modified));
|
||||
}
|
||||
|
||||
@@ -572,7 +582,7 @@ impl Contact {
|
||||
|
||||
for (name, addr) in split_address_book(addr_book).into_iter() {
|
||||
let (name, addr) = sanitize_name_and_addr(name, addr);
|
||||
let name = normalize_name(name);
|
||||
let name = normalize_name(&name);
|
||||
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
@@ -1200,7 +1210,8 @@ WHERE type=? AND id IN (
|
||||
// also unblock mailinglist
|
||||
// if the contact is a mailinglist address explicitly created to allow unblocking
|
||||
if !new_blocking && contact.origin == Origin::MailinglistAddress {
|
||||
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await?
|
||||
if let Some((chat_id, _, _)) =
|
||||
chat::get_chat_id_by_grpid(context, &contact.addr).await?
|
||||
{
|
||||
chat_id.unblock(context).await?;
|
||||
}
|
||||
@@ -1289,14 +1300,35 @@ pub(crate) async fn set_status(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
|
||||
"Can not update special contact last seen timestamp"
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
|
||||
paramsv![timestamp, contact_id],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
/// - Trims the resulting string
|
||||
///
|
||||
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||
pub fn normalize_name(full_name: impl AsRef<str>) -> String {
|
||||
let full_name = full_name.as_ref().trim();
|
||||
pub fn normalize_name(full_name: &str) -> String {
|
||||
let full_name = full_name.trim();
|
||||
if full_name.is_empty() {
|
||||
return full_name.into();
|
||||
}
|
||||
@@ -1340,31 +1372,29 @@ impl Context {
|
||||
/// determine whether the specified addr maps to the/a self addr
|
||||
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||
if let Some(self_addr) = self.get_config(Config::ConfiguredAddr).await? {
|
||||
Ok(addr_cmp(self_addr, addr))
|
||||
Ok(addr_cmp(&self_addr, addr))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
|
||||
let norm1 = addr_normalize(addr1.as_ref()).to_lowercase();
|
||||
let norm2 = addr_normalize(addr2.as_ref()).to_lowercase();
|
||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||
let norm1 = addr_normalize(addr1).to_lowercase();
|
||||
let norm2 = addr_normalize(addr2).to_lowercase();
|
||||
|
||||
norm1 == norm2
|
||||
}
|
||||
|
||||
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
book.lines()
|
||||
.collect::<Vec<&str>>()
|
||||
.chunks(2)
|
||||
.into_iter()
|
||||
.filter_map(|mut chunk| {
|
||||
let name = chunk.next().unwrap();
|
||||
let addr = match chunk.next() {
|
||||
Some(a) => a,
|
||||
None => return None,
|
||||
};
|
||||
Some((name, addr))
|
||||
.filter_map(|chunk| {
|
||||
let name = chunk.get(0)?;
|
||||
let addr = chunk.get(1)?;
|
||||
Some((*name, *addr))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1377,6 +1407,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
@@ -1495,9 +1526,9 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(t.is_self_addr("me@me.org").await?, false);
|
||||
|
||||
let addr = t.configure_alice().await;
|
||||
t.configure_addr("you@you.net").await;
|
||||
assert_eq!(t.is_self_addr("me@me.org").await?, false);
|
||||
assert_eq!(t.is_self_addr(&addr).await?, true);
|
||||
assert_eq!(t.is_self_addr("you@you.net").await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1860,7 +1891,7 @@ mod tests {
|
||||
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let id = Contact::lookup_id_by_addr(&alice.ctx, "alice@example.com", Origin::Unknown)
|
||||
let id = Contact::lookup_id_by_addr(&alice.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(id, Some(DC_CONTACT_ID_SELF));
|
||||
@@ -1885,7 +1916,7 @@ mod tests {
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_alice = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.com")
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.await;
|
||||
send_text_msg(&bob, chat_alice.id, "Hello".to_string()).await?;
|
||||
let msg = bob.pop_sent_msg().await;
|
||||
@@ -1897,7 +1928,7 @@ mod tests {
|
||||
"End-to-end encryption preferred.
|
||||
Fingerprints:
|
||||
|
||||
alice@example.com:
|
||||
alice@example.org:
|
||||
2E6F A2CB 23B5 32D7 2863
|
||||
4B58 64B0 8F61 A9ED 9443
|
||||
|
||||
@@ -1948,7 +1979,7 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
|
||||
// Bob replies.
|
||||
let chat = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.com")
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.await;
|
||||
|
||||
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
|
||||
@@ -1999,12 +2030,12 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
|
||||
alice1
|
||||
.evtracker
|
||||
.get_matching(|e| e == EventType::SelfavatarChanged)
|
||||
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
|
||||
.await;
|
||||
|
||||
// Bob sends a message so that Alice can encrypt to him.
|
||||
let chat = bob
|
||||
.create_chat_with_contact("Alice", "alice@example.com")
|
||||
.create_chat_with_contact("Alice", "alice@example.org")
|
||||
.await;
|
||||
|
||||
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
|
||||
@@ -2029,9 +2060,39 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
assert!(alice2.get_config(Config::Selfavatar).await?.is_some());
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|e| e == EventType::SelfavatarChanged)
|
||||
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_last_seen() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
|
||||
.await?;
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), 0);
|
||||
|
||||
let mime = br#"Subject: Hello
|
||||
Message-ID: message@example.net
|
||||
To: Alice <alice@example.org>
|
||||
From: Bob <bob@example.net>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
Chat-Version: 1.0
|
||||
Date: Sun, 22 Mar 2020 22:37:55 +0000
|
||||
|
||||
Hi."#;
|
||||
dc_receive_imf(&alice, mime, "Inbox", false).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
let timestamp = msg.get_timestamp();
|
||||
assert!(timestamp > 0);
|
||||
let contact = Contact::load_from_db(&alice, contact_id).await?;
|
||||
assert_eq!(contact.last_seen(), timestamp);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
141
src/context.rs
141
src/context.rs
@@ -42,12 +42,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) os_name: Option<String>,
|
||||
pub(crate) bob: Bob,
|
||||
pub(crate) last_smeared_timestamp: RwLock<i64>,
|
||||
pub(crate) running_state: RwLock<RunningState>,
|
||||
@@ -85,7 +82,7 @@ pub struct InnerContext {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RunningState {
|
||||
pub ongoing_running: bool,
|
||||
ongoing_running: bool,
|
||||
shall_stop_ongoing: bool,
|
||||
cancel_sender: Option<Sender<()>>,
|
||||
}
|
||||
@@ -107,10 +104,19 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Creates new context.
|
||||
pub async fn new(os_name: String, dbfile: PathBuf, id: u32) -> Result<Context> {
|
||||
// pretty_env_logger::try_init_timed().ok();
|
||||
/// Creates new context and opens the database.
|
||||
pub async fn new(dbfile: PathBuf, id: u32) -> Result<Context> {
|
||||
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");
|
||||
@@ -118,11 +124,38 @@ impl Context {
|
||||
if !blobdir.exists().await {
|
||||
async_std::fs::create_dir_all(&blobdir).await?;
|
||||
}
|
||||
Context::with_blobdir(os_name, 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(
|
||||
os_name: String,
|
||||
dbfile: PathBuf,
|
||||
blobdir: PathBuf,
|
||||
id: u32,
|
||||
@@ -136,10 +169,8 @@ impl Context {
|
||||
let inner = InnerContext {
|
||||
id,
|
||||
blobdir,
|
||||
dbfile,
|
||||
os_name: Some(os_name),
|
||||
running_state: RwLock::new(Default::default()),
|
||||
sql: Sql::new(),
|
||||
sql: Sql::new(dbfile),
|
||||
bob: Default::default(),
|
||||
last_smeared_timestamp: RwLock::new(0),
|
||||
generating_key_mutex: Mutex::new(()),
|
||||
@@ -158,7 +189,6 @@ impl Context {
|
||||
let ctx = Context {
|
||||
inner: Arc::new(inner),
|
||||
};
|
||||
ctx.sql.open(&ctx, &ctx.dbfile, false).await?;
|
||||
|
||||
Ok(ctx)
|
||||
}
|
||||
@@ -196,7 +226,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.
|
||||
@@ -227,7 +257,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.");
|
||||
}
|
||||
@@ -243,7 +273,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;
|
||||
|
||||
@@ -252,7 +282,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;
|
||||
|
||||
@@ -277,7 +307,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
|
||||
}
|
||||
|
||||
@@ -325,11 +355,10 @@ impl Context {
|
||||
Err(err) => format!("<key failure: {}>", err),
|
||||
};
|
||||
|
||||
let inbox_watch = self.get_config_int(Config::InboxWatch).await?;
|
||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
|
||||
let mvbox_watch = self.get_config_int(Config::MvboxWatch).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")
|
||||
@@ -355,6 +384,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()));
|
||||
@@ -384,11 +420,10 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert("inbox_watch", inbox_watch.to_string());
|
||||
res.insert("sentbox_watch", sentbox_watch.to_string());
|
||||
res.insert("mvbox_watch", mvbox_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);
|
||||
@@ -581,14 +616,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");
|
||||
@@ -645,16 +680,21 @@ mod tests {
|
||||
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
|
||||
use crate::message::Message;
|
||||
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("FakeOs".into(), 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]
|
||||
@@ -671,7 +711,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let msg = format!(
|
||||
"From: {}\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <{}>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
@@ -681,7 +721,7 @@ mod tests {
|
||||
dc_create_outgoing_rfc724_mid(None, contact.get_addr())
|
||||
);
|
||||
println!("{}", msg);
|
||||
dc_receive_imf(t, msg.as_bytes(), "INBOX", 1, false)
|
||||
dc_receive_imf(t, msg.as_bytes(), "INBOX", false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -804,9 +844,7 @@ mod tests {
|
||||
async fn test_blobdir_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
Context::new("FakeOS".into(), dbfile.into(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
Context::new(dbfile.into(), 1).await.unwrap();
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
assert!(blobdir.is_dir());
|
||||
}
|
||||
@@ -817,7 +855,7 @@ mod tests {
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
std::fs::write(&blobdir, b"123").unwrap();
|
||||
let res = Context::new("FakeOS".into(), dbfile.into(), 1).await;
|
||||
let res = Context::new(dbfile.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -827,9 +865,7 @@ mod tests {
|
||||
let subdir = tmp.path().join("subdir");
|
||||
let dbfile = subdir.join("db.sqlite");
|
||||
let dbfile2 = dbfile.clone();
|
||||
Context::new("FakeOS".into(), dbfile.into(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
Context::new(dbfile.into(), 1).await.unwrap();
|
||||
assert!(subdir.is_dir());
|
||||
assert!(dbfile2.is_file());
|
||||
}
|
||||
@@ -839,7 +875,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir, 1).await;
|
||||
let res = Context::with_blobdir(dbfile.into(), blobdir, 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -848,7 +884,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("blobs");
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into(), 1).await;
|
||||
let res = Context::with_blobdir(dbfile.into(), blobdir.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -1010,4 +1046,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
316
src/dc_tools.rs
316
src/dc_tools.rs
@@ -5,6 +5,7 @@ use core::cmp::{max, min};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::str::from_utf8;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -12,8 +13,11 @@ 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;
|
||||
use mailparse::MailHeaderMap;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::chat::{add_device_msg, add_device_msg_with_importance};
|
||||
@@ -191,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.
|
||||
@@ -350,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>,
|
||||
@@ -504,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)
|
||||
@@ -670,13 +572,144 @@ pub fn remove_subject_prefix(last_subject: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
// Types and methods to create hop-info for message-info
|
||||
|
||||
fn extract_address_from_receive_header<'a>(header: &'a str, start: &str) -> Option<&'a str> {
|
||||
let header_len = header.len();
|
||||
header.find(start).and_then(|mut begin| {
|
||||
begin += start.len();
|
||||
let end = header
|
||||
.get(begin..)?
|
||||
.find(|c: char| c.is_whitespace())
|
||||
.unwrap_or(header_len);
|
||||
header.get(begin..begin + end)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn parse_receive_header(header: &str) -> String {
|
||||
let header = header.replace(&['\r', '\n'][..], "");
|
||||
let mut hop_info = String::from("Hop: ");
|
||||
|
||||
if let Some(from) = extract_address_from_receive_header(&header, "from ") {
|
||||
hop_info += &format!("From: {}; ", from.trim());
|
||||
}
|
||||
|
||||
if let Some(by) = extract_address_from_receive_header(&header, "by ") {
|
||||
hop_info += &format!("By: {}; ", by.trim());
|
||||
}
|
||||
|
||||
if let Ok(date) = dateparse(&header) {
|
||||
// In tests, use the UTC timezone so that the test is reproducible
|
||||
#[cfg(test)]
|
||||
let date_obj = chrono::Utc.timestamp(date, 0);
|
||||
#[cfg(not(test))]
|
||||
let date_obj = Local.timestamp(date, 0);
|
||||
|
||||
hop_info += &format!("Date: {}", date_obj.to_rfc2822());
|
||||
};
|
||||
|
||||
hop_info
|
||||
}
|
||||
|
||||
/// parses "receive"-headers
|
||||
pub(crate) fn parse_receive_headers(headers: &Headers) -> String {
|
||||
headers
|
||||
.get_all_headers("Received")
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|header_map_item| from_utf8(header_map_item.get_value_raw()).ok())
|
||||
.map(parse_receive_header)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::{
|
||||
config::Config, dc_receive_imf::dc_receive_imf, message::get_msg_info,
|
||||
test_utils::TestContext,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_receive_headers() {
|
||||
// Test `parse_receive_headers()` with some more-or-less random emails from the test-data
|
||||
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
|
||||
let expected =
|
||||
"Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000\n\
|
||||
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
|
||||
check_parse_receive_headers(raw, expected);
|
||||
|
||||
let raw = include_bytes!("../test-data/message/wrong-html.eml");
|
||||
let expected =
|
||||
"Hop: From: oxbsltgw18.schlund.de; By: mrelayeu.kundenserver.de; Date: Thu, 06 Aug 2020 16:40:31 +0000\n\
|
||||
Hop: From: mout.kundenserver.de; By: dd37930.kasserver.com; Date: Thu, 06 Aug 2020 16:40:32 +0000";
|
||||
check_parse_receive_headers(raw, expected);
|
||||
|
||||
let raw = include_bytes!("../test-data/message/posteo_ndn.eml");
|
||||
let expected =
|
||||
"Hop: By: mout01.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\
|
||||
Hop: From: mout01.posteo.de; By: mx04.posteo.de; Date: Tue, 09 Jun 2020 18:44:22 +0000\n\
|
||||
Hop: From: mx04.posteo.de; By: mailin06.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
|
||||
Hop: From: mailin06.posteo.de; By: proxy02.posteo.de; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
|
||||
Hop: From: proxy02.posteo.de; By: proxy02.posteo.name; Date: Tue, 09 Jun 2020 18:44:23 +0000\n\
|
||||
Hop: From: proxy02.posteo.name; By: dovecot03.posteo.local; Date: Tue, 09 Jun 2020 18:44:24 +0000";
|
||||
check_parse_receive_headers(raw, expected);
|
||||
}
|
||||
|
||||
fn check_parse_receive_headers(raw: &[u8], expected: &str) {
|
||||
let mail = mailparse::parse_mail(raw).unwrap();
|
||||
let hop_info = parse_receive_headers(&mail.get_headers());
|
||||
assert_eq!(hop_info, expected)
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_receive_headers_integration() {
|
||||
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
|
||||
let expected = r"State: Fresh
|
||||
|
||||
hi
|
||||
|
||||
Message-ID: 2dfdbde7@example.org
|
||||
|
||||
Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000
|
||||
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
|
||||
check_parse_receive_headers_integration(raw, expected).await;
|
||||
|
||||
let raw = include_bytes!("../test-data/message/encrypted_with_received_headers.eml");
|
||||
let expected = "State: Fresh, Encrypted
|
||||
|
||||
Re: Message from alice@example.org
|
||||
|
||||
hi back\r\n\
|
||||
\r\n\
|
||||
-- \r\n\
|
||||
Sent with my Delta Chat Messenger: https://delta.chat
|
||||
|
||||
Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net
|
||||
|
||||
Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000
|
||||
Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
|
||||
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000";
|
||||
check_parse_receive_headers_integration(raw, expected).await;
|
||||
}
|
||||
|
||||
async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
|
||||
dc_receive_imf(&t, raw, "INBOX", false).await.unwrap();
|
||||
let msg = t.get_last_msg().await;
|
||||
let msg_info = get_msg_info(&t, msg.id).await.unwrap();
|
||||
|
||||
// Ignore the first rows of the msg_info because they contain a
|
||||
// received time that depends on the test time which makes it impossible to
|
||||
// compare with a static string
|
||||
let capped_result = &msg_info[msg_info.find("State").unwrap()..];
|
||||
assert_eq!(expected, capped_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rust_ftoa() {
|
||||
@@ -729,26 +762,6 @@ mod tests {
|
||||
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
|
||||
@@ -890,20 +903,7 @@ mod tests {
|
||||
|
||||
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());
|
||||
@@ -1029,7 +1029,7 @@ mod tests {
|
||||
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();
|
||||
@@ -1067,7 +1067,7 @@ mod tests {
|
||||
.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();
|
||||
@@ -1099,7 +1099,7 @@ mod tests {
|
||||
.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();
|
||||
@@ -1121,7 +1121,7 @@ mod tests {
|
||||
.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();
|
||||
@@ -1138,7 +1138,7 @@ mod tests {
|
||||
.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();
|
||||
|
||||
@@ -36,9 +36,9 @@ impl Dehtml {
|
||||
""
|
||||
}
|
||||
}
|
||||
fn append_prefix(&self, line_end: impl AsRef<str>) -> String {
|
||||
fn append_prefix(&self, line_end: &str) -> String {
|
||||
// line_end is e.g. "\n\n". We add "> " if necessary.
|
||||
line_end.as_ref().to_owned() + self.line_prefix()
|
||||
line_end.to_string() + self.line_prefix()
|
||||
}
|
||||
fn get_add_text(&self) -> AddText {
|
||||
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
|
||||
|
||||
122
src/download.rs
122
src/download.rs
@@ -129,24 +129,48 @@ impl Job {
|
||||
}
|
||||
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
let server_folder = msg.server_folder.unwrap_or_default();
|
||||
match imap
|
||||
.fetch_single_msg(context, &server_folder, msg.server_uid)
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
job_try!(
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await
|
||||
);
|
||||
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
|
||||
}
|
||||
ImapActionResult::Success | ImapActionResult::AlreadyDone => {
|
||||
// update_download_state() not needed as receive_imf() already
|
||||
// set the state and emitted the event.
|
||||
Status::Finished(Ok(()))
|
||||
let row = job_try!(
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
|
||||
paramsv![msg.rfc724_mid],
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
Ok((server_uid, server_folder))
|
||||
}
|
||||
)
|
||||
.await
|
||||
);
|
||||
|
||||
if let Some((server_uid, server_folder)) = row {
|
||||
match imap
|
||||
.fetch_single_msg(context, &server_folder, server_uid)
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
job_try!(
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await
|
||||
);
|
||||
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
|
||||
}
|
||||
ImapActionResult::Success => {
|
||||
// update_download_state() not needed as receive_imf() already
|
||||
// set the state and emitted the event.
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No IMAP record found, we don't know the UID and folder.
|
||||
job_try!(
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await
|
||||
);
|
||||
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,14 +196,18 @@ impl Imap {
|
||||
// we are connected, and the folder is selected
|
||||
info!(context, "Downloading message {}/{} fully...", folder, uid);
|
||||
|
||||
let (_, error_cnt) = self
|
||||
let (last_uid, _received) = match self
|
||||
.fetch_many_msgs(context, folder, vec![uid], false, false)
|
||||
.await;
|
||||
if error_cnt > 0 {
|
||||
return ImapActionResult::Failed;
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(_) => return ImapActionResult::Failed,
|
||||
};
|
||||
if last_uid.is_none() {
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
ImapActionResult::Success
|
||||
}
|
||||
|
||||
ImapActionResult::Success
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +255,7 @@ mod tests {
|
||||
use crate::chat::send_msg;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::dc_receive_imf::dc_receive_imf_inner;
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::test_utils::TestContext;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
@@ -308,16 +337,7 @@ mod tests {
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\
|
||||
Content-Type: text/plain";
|
||||
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
header.as_bytes(),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
dc_receive_imf_inner(&t, 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");
|
||||
@@ -330,7 +350,6 @@ mod tests {
|
||||
&t,
|
||||
format!("{}\n\n100k text...", header).as_bytes(),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
@@ -343,4 +362,39 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_partial_download_and_ephemeral() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = t
|
||||
.create_chat_with_contact("bob", "bob@example.org")
|
||||
.await
|
||||
.id;
|
||||
chat_id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
|
||||
// download message from bob partially, this must not change the ephemeral timer
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
b"From: Bob <bob@example.org>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain",
|
||||
"INBOX",
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&t).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
107
src/e2ee.rs
107
src/e2ee.rs
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{bail, ensure, 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());
|
||||
@@ -179,7 +179,6 @@ pub async fn try_decrypt(
|
||||
// Possibly perform decryption
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
|
||||
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||
let mut signatures = HashSet::default();
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
peerstate
|
||||
@@ -192,14 +191,17 @@ pub async fn try_decrypt(
|
||||
}
|
||||
}
|
||||
|
||||
let out_mail = decrypt_if_autocrypt_message(
|
||||
let (out_mail, signatures) = match decrypt_if_autocrypt_message(
|
||||
context,
|
||||
mail,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
&mut signatures,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
{
|
||||
Some((out_mail, signatures)) => (Some(out_mail), signatures),
|
||||
None => (None, Default::default()),
|
||||
};
|
||||
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
// If message is not encrypted and it is not a read receipt, degrade encryption.
|
||||
@@ -275,8 +277,7 @@ async fn decrypt_if_autocrypt_message(
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail).or_else(|| get_mixed_up_mime(mail)) {
|
||||
None => {
|
||||
// not an autocrypt mime message, abort and ignore
|
||||
@@ -290,36 +291,60 @@ async fn decrypt_if_autocrypt_message(
|
||||
encrypted_data_part,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
ret_valid_signatures,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
|
||||
///
|
||||
/// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key
|
||||
/// fingerprints for which there is a valid signature.
|
||||
async fn validate_detached_signature(
|
||||
mail: &ParsedMail<'_>,
|
||||
public_keyring_for_validate: &Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
if mail.ctype.mimetype != "multipart/signed" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
// First part is the content, second part is the signature.
|
||||
let content = first_part.raw_bytes;
|
||||
let signature = second_part.get_body_raw()?;
|
||||
let ret_valid_signatures =
|
||||
pgp::pk_validate(content, &signature, public_keyring_for_validate).await?;
|
||||
|
||||
Ok(Some((content.to_vec(), ret_valid_signatures)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Ok(None) if nothing encrypted was found.
|
||||
async fn decrypt_part(
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let data = mail.get_body_raw()?;
|
||||
|
||||
if has_decrypted_pgp_armor(&data) {
|
||||
// we should only have one decryption happening
|
||||
ensure!(ret_valid_signatures.is_empty(), "corrupt signatures");
|
||||
let (plain, ret_valid_signatures) =
|
||||
pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate).await?;
|
||||
|
||||
let plain = pgp::pk_decrypt(
|
||||
data,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
Some(ret_valid_signatures),
|
||||
)
|
||||
.await?;
|
||||
// Check for detached signatures.
|
||||
// If decrypted part is a multipart/signed, then there is a detached signature.
|
||||
let decrypted_part = mailparse::parse_mail(&plain)?;
|
||||
if let Some((content, valid_detached_signatures)) =
|
||||
validate_detached_signature(&decrypted_part, &public_keyring_for_validate).await?
|
||||
{
|
||||
return Ok(Some((content, valid_detached_signatures)));
|
||||
} else {
|
||||
// If the message was wrongly or not signed, still return the plain text.
|
||||
// The caller has to check the signatures then.
|
||||
|
||||
// If the message was wrongly or not signed, still return the plain text.
|
||||
// The caller has to check the signatures then.
|
||||
|
||||
return Ok(Some(plain));
|
||||
return Ok(Some((plain, ret_valid_signatures)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -365,12 +390,10 @@ 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)
|
||||
}
|
||||
@@ -391,9 +414,11 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_prexisting() {
|
||||
let t = TestContext::new().await;
|
||||
let test_addr = t.configure_alice().await;
|
||||
assert_eq!(ensure_secret_key_exists(&t).await.unwrap(), test_addr);
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
ensure_secret_key_exists(&t).await.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -464,7 +489,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
assert!(!msg.was_encrypted());
|
||||
|
||||
// Parsing a message is enough to update peerstate
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
@@ -495,28 +520,28 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message with Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.force_plaintext();
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message without Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
@@ -524,7 +549,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.org")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Reset);
|
||||
|
||||
190
src/ephemeral.rs
190
src/ephemeral.rs
@@ -52,7 +52,7 @@
|
||||
//! `MsgsChanged` event is emitted when a message deletion is due, to
|
||||
//! make UI reload displayed messages and cause actual deletion.
|
||||
//!
|
||||
//! Server deletion happens by generating IMAP deletion jobs based on
|
||||
//! Server deletion happens by updating the `imap` table based on
|
||||
//! the database entries which are expired either according to their
|
||||
//! ephemeral message timers or global `delete_server_after` setting.
|
||||
|
||||
@@ -73,7 +73,6 @@ use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
||||
use crate::events::EventType;
|
||||
use crate::job;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::stock_str;
|
||||
@@ -263,7 +262,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
|
||||
impl MsgId {
|
||||
/// Returns ephemeral message timer value for the message.
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> {
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> Result<Timer> {
|
||||
let res = match context
|
||||
.sql
|
||||
.query_get_value(
|
||||
@@ -279,7 +278,7 @@ impl MsgId {
|
||||
}
|
||||
|
||||
/// Starts ephemeral message timer for the message if it is not started yet.
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> {
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> Result<()> {
|
||||
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
|
||||
let ephemeral_timestamp = time().saturating_add(duration.into());
|
||||
|
||||
@@ -434,11 +433,8 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns ID of any expired message that should be deleted from the server.
|
||||
///
|
||||
/// It looks up the trash chat too, to find messages that are already
|
||||
/// deleted locally, but not deleted on the server.
|
||||
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Result<Option<MsgId>> {
|
||||
/// Schedules expired IMAP messages for deletion.
|
||||
pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> {
|
||||
let now = time();
|
||||
|
||||
let (threshold_timestamp, threshold_timestamp_extended) =
|
||||
@@ -452,27 +448,21 @@ pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Resul
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id FROM msgs \
|
||||
WHERE ( \
|
||||
((download_state = 0 AND timestamp < ?) OR (download_state != 0 AND timestamp < ?)) \
|
||||
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
|
||||
) \
|
||||
AND server_uid != 0 \
|
||||
AND NOT id IN (SELECT foreign_id FROM jobs WHERE action = ?)
|
||||
LIMIT 1",
|
||||
paramsv![
|
||||
threshold_timestamp,
|
||||
threshold_timestamp_extended,
|
||||
now,
|
||||
job::Action::DeleteMsgOnImap
|
||||
],
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
.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 <= ?))
|
||||
)",
|
||||
paramsv![threshold_timestamp, threshold_timestamp_extended, now],
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start ephemeral timers for seen messages if they are not started
|
||||
@@ -507,9 +497,6 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::param::Params;
|
||||
use async_std::task::sleep;
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
@@ -725,7 +712,7 @@ mod tests {
|
||||
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the
|
||||
/// timer does not result in disabling the timer on the Bob's side.
|
||||
#[async_std::test]
|
||||
async fn test_ephemeral_timer_rollback() -> anyhow::Result<()> {
|
||||
async fn test_ephemeral_timer_rollback() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
@@ -799,14 +786,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_ephemeral_delete_msgs() {
|
||||
async fn test_ephemeral_delete_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.get_self_chat().await;
|
||||
|
||||
t.send_text(chat.id, "Saved message, which we delete manually")
|
||||
.await;
|
||||
let msg = t.get_last_msg_in(chat.id).await;
|
||||
msg.id.delete_from_db(&t).await.unwrap();
|
||||
msg.id.delete_from_db(&t).await?;
|
||||
check_msg_was_deleted(&t, &chat, msg.id).await;
|
||||
|
||||
chat.id
|
||||
@@ -817,36 +804,12 @@ mod tests {
|
||||
.send_text(chat.id, "Saved message, disappearing after 1s")
|
||||
.await;
|
||||
|
||||
sleep(Duration::from_millis(1100)).await;
|
||||
async_std::task::sleep(Duration::from_millis(1100)).await;
|
||||
|
||||
// Check checks that the msg was deleted locally
|
||||
// Check that the msg was deleted locally.
|
||||
check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await;
|
||||
|
||||
// Check that the msg will be deleted on the server
|
||||
// First of all, set a server_uid so that DC thinks that it's actually possible to delete
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET server_uid=1 WHERE id=?",
|
||||
paramsv![msg.sender_msg_id],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let job = job::load_imap_deletion_job(&t).await.unwrap();
|
||||
assert_eq!(
|
||||
job,
|
||||
Some(job::Job::new(
|
||||
job::Action::DeleteMsgOnImap,
|
||||
msg.sender_msg_id.to_u32(),
|
||||
Params::new(),
|
||||
0,
|
||||
))
|
||||
);
|
||||
// Let's assume that executing the job fails on first try and the job is saved to the db
|
||||
job.unwrap().save(&t).await.unwrap();
|
||||
|
||||
// Make sure that we don't get yet another job when loading from db
|
||||
let job2 = job::load_imap_deletion_job(&t).await.unwrap();
|
||||
assert_eq!(job2, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||
@@ -874,7 +837,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_imap_deletion_msgid() -> Result<()> {
|
||||
async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
@@ -887,42 +850,98 @@ mod tests {
|
||||
(2000, now - 18 * HOUR, now - HOUR),
|
||||
(2020, now - 17 * HOUR, now + HOUR),
|
||||
] {
|
||||
let message_id = id.to_string();
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, server_uid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
paramsv![id, id, timestamp, ephemeral_timestamp],
|
||||
)
|
||||
.await?;
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
paramsv![id, message_id, timestamp, ephemeral_timestamp],
|
||||
)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
||||
paramsv![message_id, id],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(2000)));
|
||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
||||
assert_eq!(
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
paramsv![id.to_string()],
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
MsgId::new(2000).delete_from_db(&t).await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE rfc724_mid=?",
|
||||
paramsv![id.to_string()],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This should mark message 2000 for deletion.
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 2000).await?;
|
||||
remove_uid(&t, 2000).await?;
|
||||
// No other messages are marked for deletion.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", paramsv![],)
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
||||
.await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1000)));
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?;
|
||||
|
||||
MsgId::new(1000)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1000))); // delete downloadable anyway
|
||||
|
||||
MsgId::new(1000).delete_from_db(&t).await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target=folder WHERE rfc724_mid='1000'",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
||||
remove_uid(&t, 1000).await?;
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
||||
.await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1010)));
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1010).await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target=folder WHERE rfc724_mid='1010'",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
MsgId::new(1010)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None); // keep downloadable for now
|
||||
|
||||
MsgId::new(1010).delete_from_db(&t).await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
// Keep downloadable for now.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", paramsv![],)
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -936,7 +955,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <first@example.com>\n\
|
||||
@@ -944,7 +963,6 @@ mod tests {
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -957,7 +975,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <second@example.com>\n\
|
||||
@@ -966,7 +984,6 @@ mod tests {
|
||||
\n\
|
||||
second message\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -994,7 +1011,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <third@example.com>\n\
|
||||
@@ -1004,7 +1021,6 @@ mod tests {
|
||||
\n\
|
||||
> hello\n",
|
||||
"INBOX",
|
||||
3,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -9,6 +9,7 @@ use strum::EnumProperty;
|
||||
use crate::chat::ChatId;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::message::MsgId;
|
||||
use crate::webxdc::StatusUpdateId;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Events {
|
||||
@@ -326,4 +327,10 @@ pub enum EventType {
|
||||
|
||||
#[strum(props(id = "2110"))]
|
||||
SelfavatarChanged,
|
||||
|
||||
#[strum(props(id = "2120"))]
|
||||
WebxdcStatusUpdate {
|
||||
msg_id: MsgId,
|
||||
status_update_id: StatusUpdateId,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ pub enum HeaderDef {
|
||||
XMozillaDraftInfo,
|
||||
|
||||
ListId,
|
||||
ListPost,
|
||||
References,
|
||||
InReplyTo,
|
||||
Precedence,
|
||||
|
||||
11
src/html.rs
11
src/html.rs
@@ -440,9 +440,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
dc_receive_imf(&alice, raw, "INBOX", 1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
dc_receive_imf(&alice, raw, "INBOX", false).await.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::No);
|
||||
@@ -468,7 +466,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
// bob: check that bob also got the html-part of the forwarded message
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = bob.create_chat_with_contact("", "alice@example.com").await;
|
||||
let chat = bob.create_chat_with_contact("", "alice@example.org").await;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg_in(chat.get_id()).await;
|
||||
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
|
||||
@@ -491,9 +489,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
dc_receive_imf(&alice, raw, "INBOX", 1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
dc_receive_imf(&alice, raw, "INBOX", false).await.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
|
||||
// forward the message to saved-messages,
|
||||
@@ -560,7 +556,6 @@ test some special html-characters as < > and & but also " and &#x
|
||||
&t,
|
||||
include_bytes!("../test-data/message/cp1252-html.eml"),
|
||||
"INBOX",
|
||||
0,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
1546
src/imap.rs
1546
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");
|
||||
@@ -150,19 +150,18 @@ impl Imap {
|
||||
}
|
||||
if self.config.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break InterruptInfo::new(false, None);
|
||||
break InterruptInfo::new(false);
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
// 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);
|
||||
if res {
|
||||
break InterruptInfo::new(false, None);
|
||||
break InterruptInfo::new(false);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -10,7 +10,8 @@ use async_std::prelude::*;
|
||||
use super::{get_folder_meaning, get_folder_meaning_by_name};
|
||||
|
||||
impl Imap {
|
||||
pub 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,7 +21,7 @@ impl Imap {
|
||||
.await?;
|
||||
|
||||
if elapsed_secs < debounce_secs {
|
||||
return Ok(());
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
info!(context, "Starting full folder scan");
|
||||
@@ -29,7 +30,7 @@ impl Imap {
|
||||
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 watched_folders = get_watched_folders(context).await;
|
||||
let watched_folders = get_watched_folders(context).await?;
|
||||
|
||||
let mut folder_configs = BTreeMap::new();
|
||||
|
||||
@@ -71,15 +72,15 @@ 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_new_messages(context, folder.name(), false)
|
||||
self.fetch_move_delete(context, folder.name())
|
||||
.await
|
||||
.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,23 +99,27 @@ impl Imap {
|
||||
}
|
||||
|
||||
last_scan.replace(Instant::now());
|
||||
Ok(())
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_watched_folders(context: &Context) -> Vec<String> {
|
||||
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();
|
||||
let folder_watched_configured = &[
|
||||
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),
|
||||
(Config::MvboxWatch, Config::ConfiguredMvboxFolder),
|
||||
(Config::InboxWatch, Config::ConfiguredInboxFolder),
|
||||
];
|
||||
for (watched, configured) in folder_watched_configured {
|
||||
if context.get_config_bool(*watched).await.unwrap_or_default() {
|
||||
if let Ok(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);
|
||||
}
|
||||
}
|
||||
res
|
||||
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
|
||||
|
||||
339
src/imex.rs
339
src/imex.rs
@@ -19,8 +19,8 @@ use crate::config::Config;
|
||||
use crate::constants::{Viewtype, 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;
|
||||
@@ -30,7 +30,7 @@ use crate::message::{Message, MsgId};
|
||||
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()),
|
||||
@@ -238,7 +185,7 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
msg.param
|
||||
.set(Param::MimeType, "application/autocrypt-setup");
|
||||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
|
||||
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
@@ -449,7 +396,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,19 +419,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;
|
||||
}
|
||||
|
||||
/// 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<()> {
|
||||
info!(
|
||||
context,
|
||||
"Import \"{}\" to \"{}\".",
|
||||
@@ -495,12 +455,6 @@ 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();
|
||||
@@ -522,11 +476,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 +499,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 +556,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 +571,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 +596,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;
|
||||
@@ -936,9 +814,7 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_render_setup_file() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
t.configure_alice().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let msg = render_setup_file(&t, "hello").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
// Check some substrings, indicating things got substituted.
|
||||
@@ -955,11 +831,10 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_render_setup_file_newline_replace() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
t.configure_alice().await;
|
||||
let msg = render_setup_file(&t, "pw").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
assert!(msg.contains("<p>hello<br>there</p>"));
|
||||
@@ -1012,16 +887,14 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_export_and_import_key() {
|
||||
let context = TestContext::new().await;
|
||||
context.configure_alice().await;
|
||||
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().await;
|
||||
context2.configure_alice().await;
|
||||
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir).await {
|
||||
let context2 = TestContext::new_alice().await;
|
||||
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir, None).await {
|
||||
panic!("got error on import: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
706
src/job.rs
706
src/job.rs
@@ -3,30 +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 itertools::Itertools;
|
||||
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::context::Context;
|
||||
use crate::dc_tools::{dc_delete_file, dc_read_file, time};
|
||||
use crate::ephemeral::load_imap_deletion_msgid;
|
||||
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
|
||||
@@ -97,11 +91,6 @@ pub enum Action {
|
||||
// this is user initiated so it should have a fairly high priority
|
||||
UpdateRecentQuota = 140,
|
||||
|
||||
// Moving message is prioritized lower than deletion so we don't
|
||||
// bother moving message if it is already scheduled for deletion.
|
||||
MoveMsg = 200,
|
||||
DeleteMsgOnImap = 210,
|
||||
|
||||
// This job will download partially downloaded messages completely
|
||||
// and is added when download_full() is called.
|
||||
// Most messages are downloaded automatically on fetch
|
||||
@@ -116,7 +105,6 @@ pub enum Action {
|
||||
MaybeSendLocations = 5005, // low priority ...
|
||||
MaybeSendLocationsEnded = 5007,
|
||||
SendMdn = 5010,
|
||||
SendMsgToSmtp = 5901, // ... high priority
|
||||
}
|
||||
|
||||
impl Default for Action {
|
||||
@@ -134,17 +122,14 @@ impl From<Action> for Thread {
|
||||
|
||||
Housekeeping => Thread::Imap,
|
||||
FetchExistingMsgs => Thread::Imap,
|
||||
DeleteMsgOnImap => Thread::Imap,
|
||||
ResyncFolders => Thread::Imap,
|
||||
MarkseenMsgOnImap => Thread::Imap,
|
||||
MoveMsg => Thread::Imap,
|
||||
UpdateRecentQuota => Thread::Imap,
|
||||
DownloadMsg => Thread::Imap,
|
||||
|
||||
MaybeSendLocations => Thread::Smtp,
|
||||
MaybeSendLocationsEnded => Thread::Smtp,
|
||||
SendMdn => Thread::Smtp,
|
||||
SendMsgToSmtp => Thread::Smtp,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,217 +220,6 @@ 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,
|
||||
@@ -544,157 +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
|
||||
}
|
||||
|
||||
async fn move_msg(&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 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);
|
||||
}
|
||||
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
let server_folder = &job_try!(msg
|
||||
.server_folder
|
||||
.context("Can't move message out of folder if we don't know the current folder"));
|
||||
|
||||
let move_res = msg.id.needs_move(context, server_folder).await;
|
||||
let dest_folder = match move_res {
|
||||
Err(e) => {
|
||||
warn!(context, "could not load dest folder: {}", e);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!(
|
||||
context,
|
||||
"msg {} does not need to be moved from {}", msg.id, server_folder
|
||||
);
|
||||
return Status::Finished(Ok(()));
|
||||
}
|
||||
Ok(Some(config)) => match context.get_config(config).await {
|
||||
Ok(folder) => folder,
|
||||
Err(err) => {
|
||||
warn!(context, "failed to load config: {}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(dest_folder) = dest_folder {
|
||||
match imap
|
||||
.mv(context, server_folder, msg.server_uid, &dest_folder)
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater => Status::RetryLater,
|
||||
ImapActionResult::Success => {
|
||||
// Rust-Imap provides no target uid on mv, so just set it to 0, update again when precheck_imf() is called for the moved message
|
||||
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, 0).await;
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
ImapActionResult::Failed => {
|
||||
Status::Finished(Err(format_err!("IMAP action failed")))
|
||||
}
|
||||
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
|
||||
}
|
||||
} else {
|
||||
Status::Finished(Err(format_err!("No mvbox folder configured")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a message on the server.
|
||||
///
|
||||
/// `foreign_id` is a MsgId.
|
||||
///
|
||||
/// If the message is in the trash chat or hidden, this job
|
||||
/// removes database record, otherwise it only clears the
|
||||
/// `server_uid` column. If there are no more records pointing to
|
||||
/// the same message on the server, the job actually removes the
|
||||
/// message on the server.
|
||||
async fn delete_msg_on_imap(&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 msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
|
||||
if !msg.rfc724_mid.is_empty() {
|
||||
let cnt = message::rfc724_mid_cnt(context, &msg.rfc724_mid).await;
|
||||
info!(
|
||||
context,
|
||||
"Running delete job for message {} which has {} entries in the database",
|
||||
&msg.rfc724_mid,
|
||||
cnt
|
||||
);
|
||||
if cnt > 1 {
|
||||
info!(
|
||||
context,
|
||||
"The message is deleted from the server when all parts are deleted.",
|
||||
);
|
||||
} else if cnt == 0 {
|
||||
warn!(
|
||||
context,
|
||||
"The message {} has no UID on the server to delete", &msg.rfc724_mid
|
||||
);
|
||||
} else {
|
||||
/* if this is the last existing part of the message,
|
||||
we delete the message from the server */
|
||||
let mid = msg.rfc724_mid;
|
||||
let server_folder = msg.server_folder.as_ref().unwrap();
|
||||
let res = if msg.server_uid == 0 {
|
||||
// Message is already deleted on IMAP server.
|
||||
ImapActionResult::AlreadyDone
|
||||
} else {
|
||||
imap.delete_msg(context, &mid, server_folder, msg.server_uid)
|
||||
.await
|
||||
};
|
||||
match res {
|
||||
ImapActionResult::AlreadyDone | ImapActionResult::Success => {}
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
// If job has failed, for example due to some
|
||||
// IMAP bug, we postpone it instead of failing
|
||||
// immediately. This will prevent adding it
|
||||
// immediately again if user has enabled
|
||||
// automatic message deletion. Without this,
|
||||
// we might waste a lot of traffic constantly
|
||||
// retrying message deletion.
|
||||
return Status::RetryLater;
|
||||
}
|
||||
}
|
||||
}
|
||||
if msg.chat_id.is_trash() || msg.hidden {
|
||||
// Messages are stored in trash chat only to keep
|
||||
// their server UID and Message-ID. Once message is
|
||||
// deleted from the server, database record can be
|
||||
// removed as well.
|
||||
//
|
||||
// Hidden messages are similar to trashed, but are
|
||||
// related to some chat. We also delete their
|
||||
// database records.
|
||||
job_try!(msg.id.delete_from_db(context).await)
|
||||
} else {
|
||||
// Remove server UID from the database record.
|
||||
//
|
||||
// We have either just removed the message from the
|
||||
// server, in which case UID is not valid anymore, or
|
||||
// we have more refernces to the same server UID, so
|
||||
// we remove UID to reduce the number of messages
|
||||
// pointing to the corresponding UID. Once the counter
|
||||
// reaches zero, we will remove the message.
|
||||
job_try!(msg.id.unlink(context).await);
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
} else {
|
||||
/* eg. device messages have no Message-ID */
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
status
|
||||
}
|
||||
|
||||
/// Read the recipients from old emails sent by the user and add them as contacts.
|
||||
@@ -735,15 +365,7 @@ 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 sentbox, inbox and mvbox.
|
||||
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);
|
||||
@@ -775,55 +397,58 @@ impl Job {
|
||||
}
|
||||
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
|
||||
let folder = msg.server_folder.as_ref().unwrap();
|
||||
|
||||
let result = if msg.server_uid == 0 {
|
||||
// The message is moved or deleted by us.
|
||||
//
|
||||
// Do not call set_seen with zero UID, as it will return
|
||||
// ImapActionResult::RetryLater, but we do not want to
|
||||
// retry. If the message was moved, we will create another
|
||||
// job to mark the message as seen later. If it was
|
||||
// deleted, there is nothing to do.
|
||||
info!(context, "Can't mark message as seen: No UID");
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
imap.set_seen(context, folder, msg.server_uid).await
|
||||
};
|
||||
|
||||
match result {
|
||||
ImapActionResult::RetryLater => Status::RetryLater,
|
||||
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
|
||||
ImapActionResult::Success | ImapActionResult::Failed => {
|
||||
// XXX the message might just have been moved
|
||||
// we want to send out an MDN anyway
|
||||
// The job will not be retried so locally
|
||||
// there is no risk of double-sending MDNs.
|
||||
//
|
||||
// Read receipts for system messages are never
|
||||
// sent. These messages have no place to display
|
||||
// received read receipt anyway. And since their text
|
||||
// is locally generated, quoting them is dangerous as
|
||||
// it may contain contact names. E.g., for original
|
||||
// message "Group left by me", a read receipt will
|
||||
// quote "Group left by <name>", and the name can be a
|
||||
// display name stored in address book rather than
|
||||
// the name sent in the From field by the user.
|
||||
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default()
|
||||
&& !msg.is_system_message()
|
||||
{
|
||||
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
|
||||
if mdns_enabled {
|
||||
if let Err(err) = send_mdn(context, &msg).await {
|
||||
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
|
||||
return Status::Finished(Err(err));
|
||||
}
|
||||
let row = job_try!(
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder FROM imap
|
||||
WHERE rfc724_mid=? AND folder=target
|
||||
ORDER BY uid ASC
|
||||
LIMIT 1",
|
||||
paramsv![msg.rfc724_mid],
|
||||
|row| {
|
||||
let uid: u32 = row.get(0)?;
|
||||
let folder: String = row.get(1)?;
|
||||
Ok((uid, folder))
|
||||
}
|
||||
)
|
||||
.await
|
||||
);
|
||||
if let Some((server_uid, server_folder)) = row {
|
||||
let result = imap.set_seen(context, &server_folder, server_uid).await;
|
||||
match result {
|
||||
ImapActionResult::RetryLater => return Status::RetryLater,
|
||||
ImapActionResult::Success | ImapActionResult::Failed => {}
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Can't mark the message {} as seen on IMAP because there is no known UID",
|
||||
msg.rfc724_mid
|
||||
);
|
||||
}
|
||||
|
||||
// XXX we send MDN even in case of failure to mark the messages as seen, e.g. if it was
|
||||
// already deleted on the server by another device. The job will not be retried so locally
|
||||
// there is no risk of double-sending MDNs.
|
||||
//
|
||||
// Read receipts for system messages are never sent. These messages have no place to
|
||||
// display received read receipt anyway. And since their text is locally generated,
|
||||
// quoting them is dangerous as it may contain contact names. E.g., for original message
|
||||
// "Group left by me", a read receipt will quote "Group left by <name>", and the name can
|
||||
// be a display name stored in address book rather than the name sent in the From field by
|
||||
// the user.
|
||||
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default() && !msg.is_system_message() {
|
||||
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
|
||||
if mdns_enabled {
|
||||
if let Err(err) = send_mdn(context, &msg).await {
|
||||
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
|
||||
return Status::Finished(Err(err));
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -840,7 +465,7 @@ pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
|
||||
async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
|
||||
let q = format!(
|
||||
"DELETE FROM jobs WHERE id IN({})",
|
||||
job_ids.iter().map(|_| "?").join(",")
|
||||
job_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
|
||||
);
|
||||
context
|
||||
.sql
|
||||
@@ -860,17 +485,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
|
||||
@@ -889,7 +503,7 @@ async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, fold
|
||||
let display_name_normalized = contact
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(normalize_name)
|
||||
.map(|s| normalize_name(s))
|
||||
.unwrap_or_default();
|
||||
|
||||
match Contact::add_or_lookup(
|
||||
@@ -916,144 +530,11 @@ 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),
|
||||
}
|
||||
|
||||
pub(crate) async fn load_imap_deletion_job(context: &Context) -> Result<Option<Job>> {
|
||||
let res = load_imap_deletion_msgid(context)
|
||||
.await?
|
||||
.map(|msg_id| Job::new(Action::DeleteMsgOnImap, msg_id.to_u32(), Params::new(), 0));
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for Connection<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
@@ -1156,16 +637,13 @@ 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 => {
|
||||
location::job_maybe_send_locations_ended(context, job).await
|
||||
}
|
||||
Action::DeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
|
||||
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::MoveMsg => job.move_msg(context, connection.inbox()).await,
|
||||
Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
|
||||
Action::Housekeeping => {
|
||||
sql::housekeeping(context).await.ok_or_log(context);
|
||||
@@ -1222,16 +700,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;
|
||||
@@ -1242,26 +710,17 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
|
||||
match action {
|
||||
Action::Unknown => unreachable!(),
|
||||
Action::Housekeeping
|
||||
| Action::DeleteMsgOnImap
|
||||
| Action::ResyncFolders
|
||||
| Action::MarkseenMsgOnImap
|
||||
| Action::FetchExistingMsgs
|
||||
| Action::MoveMsg
|
||||
| Action::UpdateRecentQuota
|
||||
| Action::DownloadMsg => {
|
||||
info!(context, "interrupt: imap");
|
||||
context
|
||||
.interrupt_inbox(InterruptInfo::new(false, None))
|
||||
.await;
|
||||
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, None))
|
||||
.await;
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1296,20 +755,9 @@ pub(crate) async fn load_next(
|
||||
let query;
|
||||
let params;
|
||||
let t = time();
|
||||
let m;
|
||||
let thread_i = thread as i64;
|
||||
|
||||
if let Some(msg_id) = info.msg_id {
|
||||
query = r#"
|
||||
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
||||
FROM jobs
|
||||
WHERE thread=? AND foreign_id=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
m = msg_id;
|
||||
params = paramsv![thread_i, m];
|
||||
} else if !info.probe_network {
|
||||
if !info.probe_network {
|
||||
// processing for first-try and after backoff-timeouts:
|
||||
// process jobs in the order they were added.
|
||||
query = r#"
|
||||
@@ -1379,12 +827,6 @@ LIMIT 1;
|
||||
}
|
||||
Thread::Imap => {
|
||||
if let Some(job) = job {
|
||||
if job.action < Action::DeleteMsgOnImap {
|
||||
Ok(load_imap_deletion_job(context).await?.or(Some(job)))
|
||||
} else {
|
||||
Ok(Some(job))
|
||||
}
|
||||
} else if let Some(job) = load_imap_deletion_job(context).await? {
|
||||
Ok(Some(job))
|
||||
} else {
|
||||
Ok(load_housekeeping_job(context).await?)
|
||||
@@ -1410,8 +852,12 @@ mod tests {
|
||||
VALUES (?, ?, ?, ?, ?, ?);",
|
||||
paramsv![
|
||||
now,
|
||||
Thread::from(Action::MoveMsg),
|
||||
if valid { Action::MoveMsg as i32 } else { -1 },
|
||||
Thread::from(Action::DownloadMsg),
|
||||
if valid {
|
||||
Action::DownloadMsg as i32
|
||||
} else {
|
||||
-1
|
||||
},
|
||||
foreign_id,
|
||||
Params::new().to_string(),
|
||||
now
|
||||
@@ -1430,8 +876,8 @@ mod tests {
|
||||
insert_job(&t, 1, false).await; // This can not be loaded into Job struct.
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
)
|
||||
.await?;
|
||||
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
|
||||
@@ -1440,8 +886,8 @@ mod tests {
|
||||
insert_job(&t, 1, true).await;
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
)
|
||||
.await?;
|
||||
assert!(jobs.is_some());
|
||||
@@ -1456,8 +902,8 @@ mod tests {
|
||||
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
)
|
||||
.await?;
|
||||
assert!(jobs.is_some());
|
||||
|
||||
43
src/key.rs
43
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,13 +288,13 @@ 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,
|
||||
@@ -313,7 +312,7 @@ 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(())
|
||||
}
|
||||
@@ -510,8 +509,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[async_std::test]
|
||||
async fn test_load_self_existing() {
|
||||
let alice = alice_keypair();
|
||||
let t = TestContext::new().await;
|
||||
t.configure_alice().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let pubkey = SignedPublicKey::load_self(&t).await.unwrap();
|
||||
assert_eq!(alice.public, pubkey);
|
||||
let seckey = SignedSecretKey::load_self(&t).await.unwrap();
|
||||
@@ -521,7 +519,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[async_std::test]
|
||||
async fn test_load_self_generate_public() {
|
||||
let t = TestContext::new().await;
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = SignedPublicKey::load_self(&t).await;
|
||||
@@ -531,7 +529,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[async_std::test]
|
||||
async fn test_load_self_generate_secret() {
|
||||
let t = TestContext::new().await;
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = SignedSecretKey::load_self(&t).await;
|
||||
@@ -543,7 +541,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
use std::thread;
|
||||
|
||||
let t = TestContext::new().await;
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
let thr0 = {
|
||||
@@ -589,27 +587,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
assert_eq!(nrows().await, 1);
|
||||
}
|
||||
|
||||
// Convenient way to create a new key if you need one, run with
|
||||
// `cargo test key::tests::gen_key`.
|
||||
// #[test]
|
||||
// fn gen_key() {
|
||||
// let name = "fiona";
|
||||
// let keypair = crate::pgp::create_keypair(
|
||||
// EmailAddress::new(&format!("{}@example.net", name)).unwrap(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// std::fs::write(
|
||||
// format!("test-data/key/{}-public.asc", name),
|
||||
// keypair.public.to_base64(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// std::fs::write(
|
||||
// format!("test-data/key/{}-secret.asc", name),
|
||||
// keypair.secret.to_base64(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_fingerprint_from_str() {
|
||||
let res = Fingerprint::new(vec![
|
||||
|
||||
@@ -79,8 +79,7 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_keyring_load_self() {
|
||||
// new_self() implies load_self()
|
||||
let t = TestContext::new().await;
|
||||
t.configure_alice().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let alice = alice_keypair();
|
||||
|
||||
let pub_ring: Keyring<SignedPublicKey> = Keyring::new_self(&t).await.unwrap();
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::eval_order_dependence,
|
||||
clippy::bool_assert_comparison
|
||||
clippy::bool_assert_comparison,
|
||||
clippy::manual_split_once
|
||||
)]
|
||||
|
||||
#[macro_use]
|
||||
@@ -77,6 +78,7 @@ pub mod peerstate;
|
||||
pub mod pgp;
|
||||
pub mod provider;
|
||||
pub mod qr;
|
||||
pub mod qr_code_generator;
|
||||
pub mod quota;
|
||||
pub mod securejoin;
|
||||
mod simplify;
|
||||
@@ -85,6 +87,7 @@ pub mod stock_str;
|
||||
mod sync;
|
||||
mod token;
|
||||
mod update_helper;
|
||||
pub mod webxdc;
|
||||
#[macro_use]
|
||||
mod dehtml;
|
||||
mod color;
|
||||
|
||||
@@ -223,7 +223,7 @@ pub async fn send_locations_to_chat(
|
||||
.unwrap_or_default();
|
||||
} else if 0 == seconds && is_sending_locations_before {
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
chat::add_info_msg(context, chat_id, stock_str, now).await?;
|
||||
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if 0 != seconds {
|
||||
@@ -747,7 +747,7 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
);
|
||||
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
job_try!(chat::add_info_msg(context, chat_id, stock_str, now).await);
|
||||
job_try!(chat::add_info_msg(context, chat_id, &stock_str, now).await);
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,7 +413,7 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let param = LoginParam {
|
||||
addr: "alice@example.com".to_string(),
|
||||
addr: "alice@example.org".to_string(),
|
||||
imap: ServerLoginParam {
|
||||
server: "imap.example.com".to_string(),
|
||||
user: "alice".to_string(),
|
||||
@@ -424,7 +424,7 @@ mod tests {
|
||||
},
|
||||
smtp: ServerLoginParam {
|
||||
server: "smtp.example.com".to_string(),
|
||||
user: "alice@example.com".to_string(),
|
||||
user: "alice@example.org".to_string(),
|
||||
password: "bar".to_string(),
|
||||
port: 456,
|
||||
security: Socket::Ssl,
|
||||
|
||||
548
src/message.rs
548
src/message.rs
@@ -1,6 +1,6 @@
|
||||
//! # Messages and their identifiers.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use anyhow::{ensure, format_err, Context as _, Result};
|
||||
@@ -10,7 +10,6 @@ use rusqlite::types::ValueRef;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
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,
|
||||
@@ -29,6 +28,7 @@ use crate::log::LogExt;
|
||||
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::stock_str;
|
||||
use crate::summary::Summary;
|
||||
|
||||
@@ -83,65 +83,6 @@ impl MsgId {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns Some if the message needs to be moved from `folder`.
|
||||
/// If yes, returns `ConfiguredInboxFolder`, `ConfiguredMvboxFolder` or `ConfiguredSentboxFolder`,
|
||||
/// depending on where the message should be moved
|
||||
pub async fn needs_move(self, context: &Context, folder: &str) -> Result<Option<Config>> {
|
||||
use Config::*;
|
||||
if context.is_mvbox(folder).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
|
||||
if context.is_spam_folder(folder).await? {
|
||||
let msg_unblocked = msg.chat_id != DC_CHAT_ID_TRASH && msg.chat_blocked == Blocked::Not;
|
||||
|
||||
return if msg_unblocked {
|
||||
if self.needs_move_to_mvbox(context, &msg).await? {
|
||||
Ok(Some(ConfiguredMvboxFolder))
|
||||
} else {
|
||||
Ok(Some(ConfiguredInboxFolder))
|
||||
}
|
||||
} else {
|
||||
// Blocked or contact request message in the spam folder, leave it there
|
||||
Ok(None)
|
||||
};
|
||||
}
|
||||
|
||||
if self.needs_move_to_mvbox(context, &msg).await? {
|
||||
Ok(Some(ConfiguredMvboxFolder))
|
||||
} else if msg.state.is_outgoing()
|
||||
&& msg.is_dc_message == MessengerMessage::Yes
|
||||
&& !msg.is_setupmessage()
|
||||
&& msg.to_id != DC_CONTACT_ID_SELF // Leave self-chat-messages in the inbox, not sure about this
|
||||
&& context.is_inbox(folder).await?
|
||||
&& context.get_config_bool(SentboxMove).await?
|
||||
&& context.get_config(ConfiguredSentboxFolder).await?.is_some()
|
||||
{
|
||||
Ok(Some(ConfiguredSentboxFolder))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn needs_move_to_mvbox(self, context: &Context, msg: &Message) -> Result<bool> {
|
||||
if !context.get_config_bool(Config::MvboxMove).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if msg.is_setupmessage() {
|
||||
// do not move setup messages;
|
||||
// there may be a non-delta device that wants to handle it
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match msg.is_dc_message {
|
||||
MessengerMessage::No => Ok(false),
|
||||
MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
|
||||
}
|
||||
}
|
||||
|
||||
/// Put message into trash chat and delete message text.
|
||||
///
|
||||
/// It means the message is deleted locally, but not on the server.
|
||||
@@ -172,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])
|
||||
@@ -187,21 +139,17 @@ WHERE id=?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes IMAP server UID and folder from the database record.
|
||||
///
|
||||
/// It is used to avoid trying to remove the message from the
|
||||
/// server multiple times when there are multiple message records
|
||||
/// pointing to the same server UID.
|
||||
pub(crate) async fn unlink(self, context: &Context) -> Result<()> {
|
||||
context
|
||||
pub(crate) async fn set_delivered(self, context: &Context) -> Result<()> {
|
||||
update_msg_state(context, self, MessageState::OutDelivered).await?;
|
||||
let chat_id: ChatId = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET server_folder='', server_uid=0 \
|
||||
WHERE id=?",
|
||||
paramsv![self],
|
||||
)
|
||||
.await?;
|
||||
.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(())
|
||||
}
|
||||
|
||||
@@ -232,7 +180,7 @@ 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);
|
||||
@@ -308,8 +256,6 @@ pub struct Message {
|
||||
pub(crate) subject: String,
|
||||
pub(crate) rfc724_mid: String,
|
||||
pub(crate) in_reply_to: Option<String>,
|
||||
pub(crate) server_folder: Option<String>,
|
||||
pub(crate) server_uid: u32,
|
||||
pub(crate) is_dc_message: MessengerMessage,
|
||||
pub(crate) mime_modified: bool,
|
||||
pub(crate) chat_blocked: Blocked,
|
||||
@@ -329,7 +275,7 @@ impl Message {
|
||||
pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
|
||||
ensure!(
|
||||
!id.is_special(),
|
||||
"Can not load special message ID {} from DB.",
|
||||
"Can not load special message ID {} from DB",
|
||||
id
|
||||
);
|
||||
let msg = context
|
||||
@@ -340,8 +286,6 @@ impl Message {
|
||||
" m.id AS id,",
|
||||
" rfc724_mid AS rfc724mid,",
|
||||
" m.mime_in_reply_to AS mime_in_reply_to,",
|
||||
" m.server_folder AS server_folder,",
|
||||
" m.server_uid AS server_uid,",
|
||||
" m.chat_id AS chat_id,",
|
||||
" m.from_id AS from_id,",
|
||||
" m.to_id AS to_id,",
|
||||
@@ -392,8 +336,6 @@ impl Message {
|
||||
in_reply_to: row
|
||||
.get::<_, Option<String>>("mime_in_reply_to")?
|
||||
.and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
|
||||
server_folder: row.get::<_, Option<String>>("server_folder")?,
|
||||
server_uid: row.get("server_uid")?,
|
||||
chat_id: row.get("chat_id")?,
|
||||
from_id: row.get("from_id")?,
|
||||
to_id: row.get("to_id")?,
|
||||
@@ -839,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(())
|
||||
}
|
||||
|
||||
@@ -877,22 +824,32 @@ impl Message {
|
||||
}
|
||||
|
||||
pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
|
||||
if self.param.get(Param::Quote).is_some() {
|
||||
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))
|
||||
};
|
||||
}
|
||||
if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
|
||||
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)
|
||||
}
|
||||
|
||||
/// Force the message to be sent in plain text.
|
||||
pub fn force_plaintext(&mut self) {
|
||||
self.param.set_int(Param::ForcePlaintext, 1);
|
||||
}
|
||||
|
||||
pub async fn update_param(&self, context: &Context) {
|
||||
context
|
||||
.sql
|
||||
@@ -1162,11 +1119,13 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
|
||||
if !msg.rfc724_mid.is_empty() {
|
||||
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
|
||||
}
|
||||
if let Some(ref server_folder) = msg.server_folder {
|
||||
if !server_folder.is_empty() {
|
||||
ret += &format!("\nLast seen as: {}/{}", server_folder, msg.server_uid);
|
||||
}
|
||||
}
|
||||
let hop_info: Option<String> = context
|
||||
.sql
|
||||
.query_get_value("SELECT hop_info FROM msgs WHERE id=?;", paramsv![msg_id])
|
||||
.await?;
|
||||
|
||||
ret += "\n\n";
|
||||
ret += &hop_info.unwrap_or_else(|| "No Hop Info".to_owned());
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
@@ -1232,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,
|
||||
@@ -1283,11 +1243,13 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
.trash(context)
|
||||
.await
|
||||
.with_context(|| format!("Unable to trash message {}", msg_id))?;
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::DeleteMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target='' WHERE rfc724_mid=?",
|
||||
paramsv![msg.rfc724_mid],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !msg_ids.is_empty() {
|
||||
@@ -1302,6 +1264,9 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Interrupt Inbox loop to start message deletion.
|
||||
context.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1354,7 +1319,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut updated_chat_ids = BTreeMap::new();
|
||||
let mut updated_chat_ids = BTreeSet::new();
|
||||
|
||||
for (id, curr_chat_id, curr_state, curr_blocked) in msgs.into_iter() {
|
||||
if let Err(err) = id.start_ephemeral_timer(context).await {
|
||||
@@ -1368,7 +1333,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
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(
|
||||
@@ -1376,26 +1341,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.
|
||||
@@ -1527,7 +1496,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)
|
||||
@@ -1596,9 +1565,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)
|
||||
@@ -1606,7 +1573,7 @@ async fn ndn_maybe_add_info_msg(
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
chat_id,
|
||||
text,
|
||||
&text,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
)
|
||||
.await?;
|
||||
@@ -1682,7 +1649,7 @@ pub async fn estimate_deletion_cnt(
|
||||
WHERE m.id > ?
|
||||
AND timestamp < ?
|
||||
AND chat_id != ?
|
||||
AND server_uid != 0;",
|
||||
AND EXISTS (SELECT * FROM imap WHERE rfc724_mid=m.rfc724_mid);",
|
||||
paramsv![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id],
|
||||
)
|
||||
.await?
|
||||
@@ -1708,32 +1675,10 @@ pub async fn estimate_deletion_cnt(
|
||||
Ok(cnt)
|
||||
}
|
||||
|
||||
/// Counts number of database records pointing to specified
|
||||
/// Message-ID.
|
||||
///
|
||||
/// Unlinked messages are excluded.
|
||||
pub async fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> usize {
|
||||
// check the number of messages with the same rfc724_mid
|
||||
match context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0",
|
||||
paramsv![rfc724_mid],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
error!(context, "dc_get_rfc724_mid_cnt() failed. {}", err);
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn rfc724_mid_exists(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
) -> Result<Option<(String, u32, MsgId)>> {
|
||||
) -> Result<Option<MsgId>> {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
if rfc724_mid.is_empty() {
|
||||
warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
|
||||
@@ -1743,14 +1688,12 @@ pub(crate) async fn rfc724_mid_exists(
|
||||
let res = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?",
|
||||
"SELECT id FROM msgs WHERE rfc724_mid=?",
|
||||
paramsv![rfc724_mid],
|
||||
|row| {
|
||||
let server_folder = row.get::<_, Option<String>>(0)?.unwrap_or_default();
|
||||
let server_uid = row.get(1)?;
|
||||
let msg_id: MsgId = row.get(2)?;
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
|
||||
Ok((server_folder, server_uid, msg_id))
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -1758,28 +1701,6 @@ pub(crate) async fn rfc724_mid_exists(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn update_server_uid(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
server_folder: &str,
|
||||
server_uid: u32,
|
||||
) {
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET server_folder=?, server_uid=? \
|
||||
WHERE rfc724_mid=?",
|
||||
paramsv![server_folder, server_uid, rfc724_mid],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
warn!(context, "msg: failed to update server_uid: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1796,212 +1717,14 @@ mod tests {
|
||||
guess_msgtype_from_suffix(Path::new("foo/bar-sth.mp3")),
|
||||
Some((Viewtype::Audio, "audio/mpeg"))
|
||||
);
|
||||
}
|
||||
|
||||
// chat_msg means that the message was sent by Delta Chat
|
||||
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
|
||||
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", false, true, "INBOX"),
|
||||
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", true, true, "DeltaChat"),
|
||||
];
|
||||
|
||||
// These are the same as above, but all messages in Spam stay in Spam
|
||||
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "Spam"),
|
||||
("Spam", false, true, "Spam"),
|
||||
("Spam", true, false, "Spam"),
|
||||
("Spam", true, true, "Spam"),
|
||||
];
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_needs_move_incoming_accepted() {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_needs_move_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_needs_move_incoming_request() {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
|
||||
check_needs_move_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_needs_move_outgoing() {
|
||||
for sentbox_move in &[true, false] {
|
||||
// Test outgoing emails
|
||||
for (folder, mvbox_move, chat_msg, mut expected_destination) in
|
||||
COMBINATIONS_ACCEPTED_CHAT
|
||||
{
|
||||
if *folder == "INBOX" && !mvbox_move && *chat_msg && *sentbox_move {
|
||||
expected_destination = "Sent"
|
||||
}
|
||||
check_needs_move_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
*sentbox_move,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_needs_move_setupmsg() {
|
||||
// Test setupmessages
|
||||
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_needs_move_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn check_needs_move_combination(
|
||||
folder: &str,
|
||||
mvbox_move: bool,
|
||||
chat_msg: bool,
|
||||
expected_destination: &str,
|
||||
accepted_chat: bool,
|
||||
outgoing: bool,
|
||||
setupmessage: bool,
|
||||
sentbox_move: bool,
|
||||
) {
|
||||
println!("Testing: For folder {}, mvbox_move {}, chat_msg {}, accepted {}, outgoing {}, setupmessage {}",
|
||||
folder, mvbox_move, chat_msg, accepted_chat, outgoing, setupmessage);
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredSpamFolder, Some("Spam"))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredSentboxFolder, Some("Sent"))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
.await
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config_bool(Config::SentboxMove, sentbox_move)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if accepted_chat {
|
||||
let contact_id = Contact::create(&t.ctx, "", "bob@example.net")
|
||||
.await
|
||||
.unwrap();
|
||||
ChatId::create_for_contact(&t.ctx, contact_id)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let temp;
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
if setupmessage {
|
||||
include_bytes!("../test-data/message/AutocryptSetupMessage.eml")
|
||||
} else {
|
||||
temp = format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
{}\
|
||||
Subject: foo\n\
|
||||
Message-ID: <abc@example.com>\n\
|
||||
{}\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
if outgoing {
|
||||
"From: alice@example.com\nTo: bob@example.net\n"
|
||||
} else {
|
||||
"From: bob@example.net\nTo: alice@example.com\n"
|
||||
},
|
||||
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
|
||||
);
|
||||
temp.as_bytes()
|
||||
},
|
||||
folder,
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let exists = rfc724_mid_exists(&t, "abc@example.com").await.unwrap();
|
||||
let (folder_1, _, msg_id) = exists.unwrap();
|
||||
assert_eq!(folder, folder_1);
|
||||
let actual = if let Some(config) = msg_id.needs_move(&t.ctx, folder).await.unwrap() {
|
||||
t.ctx.get_config(config).await.unwrap()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let expected = if expected_destination == folder {
|
||||
None
|
||||
} else {
|
||||
Some(expected_destination)
|
||||
};
|
||||
assert_eq!(expected, actual.as_deref(), "For folder {}, mvbox_move {}, chat_msg {}, accepted {}, outgoing {}, setupmessage {}: expected {:?}, got {:?}",
|
||||
folder, mvbox_move, chat_msg, accepted_chat, outgoing, setupmessage, expected, actual);
|
||||
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]
|
||||
@@ -2169,7 +1892,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
|
||||
@@ -2187,14 +1912,13 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <123@example.com>\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
123,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
@@ -2301,9 +2025,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);
|
||||
|
||||
@@ -2367,7 +2091,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;
|
||||
@@ -2396,7 +2120,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <123@example.com>\n\
|
||||
Auto-Submitted: auto-generated\n\
|
||||
@@ -2404,7 +2128,6 @@ mod tests {
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -2416,14 +2139,13 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <456@example.com>\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello again\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Result};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use chrono::TimeZone;
|
||||
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
|
||||
|
||||
@@ -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,
|
||||
@@ -154,6 +154,12 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
if chat.is_self_talk() {
|
||||
recipients.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
} else if chat.is_mailing_list() {
|
||||
let list_post = chat
|
||||
.param
|
||||
.get(Param::ListPost)
|
||||
.context("Can't write to mailinglist without ListPost param")?;
|
||||
recipients.push(("".to_string(), list_post.to_string()));
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
@@ -201,7 +207,6 @@ impl<'a> MimeFactory<'a> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let default_str = stock_str::status_line(context).await;
|
||||
let factory = MimeFactory {
|
||||
from_addr,
|
||||
from_displayname,
|
||||
@@ -209,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 },
|
||||
@@ -240,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> {
|
||||
@@ -277,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
|
||||
@@ -458,20 +462,45 @@ impl<'a> MimeFactory<'a> {
|
||||
self.from_addr.clone(),
|
||||
);
|
||||
|
||||
let mut to = Vec::new();
|
||||
for (name, addr) in self.recipients.iter() {
|
||||
if name.is_empty() {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
} else {
|
||||
to.push(Address::new_mailbox_with_name(
|
||||
name.to_string(),
|
||||
addr.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let undisclosed_recipients = match &self.loaded {
|
||||
Loaded::Message { chat } => chat.typ == Chattype::Broadcast,
|
||||
Loaded::Mdn { .. } => false,
|
||||
};
|
||||
|
||||
if to.is_empty() {
|
||||
to.push(from.clone());
|
||||
let mut to = Vec::new();
|
||||
if undisclosed_recipients {
|
||||
to.push(Address::new_group(
|
||||
"hidden-recipients".to_string(),
|
||||
Vec::new(),
|
||||
));
|
||||
} else {
|
||||
let email_to_remove =
|
||||
if self.msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
self.msg.param.get(Param::Arg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for (name, addr) in self.recipients.iter() {
|
||||
if let Some(email_to_remove) = email_to_remove {
|
||||
if email_to_remove == addr {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if name.is_empty() {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
} else {
|
||||
to.push(Address::new_mailbox_with_name(
|
||||
name.to_string(),
|
||||
addr.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if to.is_empty() {
|
||||
to.push(from.clone());
|
||||
}
|
||||
}
|
||||
|
||||
headers
|
||||
@@ -572,20 +601,9 @@ impl<'a> MimeFactory<'a> {
|
||||
render_rfc724_mid(&rfc724_mid),
|
||||
));
|
||||
|
||||
let undisclosed_recipients = match &self.loaded {
|
||||
Loaded::Message { chat } => chat.typ == Chattype::Broadcast,
|
||||
Loaded::Mdn { .. } => false,
|
||||
};
|
||||
|
||||
if undisclosed_recipients {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("To".into(), "hidden-recipients: ;".to_string()));
|
||||
} else {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("To".into(), to).unwrap());
|
||||
}
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("To".into(), to).unwrap());
|
||||
|
||||
headers
|
||||
.unprotected
|
||||
@@ -623,6 +641,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)
|
||||
};
|
||||
@@ -747,7 +770,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,
|
||||
@@ -897,7 +920,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.
|
||||
//
|
||||
@@ -1134,6 +1159,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 {
|
||||
@@ -1265,7 +1300,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
|
||||
@@ -1293,8 +1328,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(),
|
||||
@@ -1405,6 +1439,10 @@ mod tests {
|
||||
use async_std::prelude::*;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::chat::{
|
||||
add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::contact::Origin;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
@@ -1412,7 +1450,7 @@ mod tests {
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
|
||||
use async_std::fs::File;
|
||||
use pretty_assertions::assert_eq;
|
||||
use mailparse::{addrparse_header, MailHeaderMap};
|
||||
|
||||
#[test]
|
||||
fn test_render_email_address() {
|
||||
@@ -1510,7 +1548,7 @@ mod tests {
|
||||
msg_to_subject_str(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: Bob <bob@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Antw: Chat: hello\n\
|
||||
Message-ID: <2222@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
@@ -1525,7 +1563,7 @@ mod tests {
|
||||
msg_to_subject_str(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: Bob <bob@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Infos: 42\n\
|
||||
Message-ID: <2222@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
@@ -1544,7 +1582,7 @@ mod tests {
|
||||
msg_to_subject_str(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Chat: hello\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2223@example.com>\n\
|
||||
@@ -1562,7 +1600,7 @@ mod tests {
|
||||
// 3. Send the first message to a new contact
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
assert_eq!(first_subject_str(t).await, "Message from alice@example.com");
|
||||
assert_eq!(first_subject_str(t).await, "Message from alice@example.org");
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::Displayname, Some("Alice"))
|
||||
@@ -1577,7 +1615,7 @@ mod tests {
|
||||
msg_to_subject_str(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: äääää\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2893@example.com>\n\
|
||||
@@ -1591,7 +1629,7 @@ mod tests {
|
||||
msg_to_subject_str(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: aäääää\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2893@example.com>\n\
|
||||
@@ -1610,7 +1648,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: alice@example.com\n\
|
||||
From: alice@example.org\n\
|
||||
To: bob@example.com\n\
|
||||
Subject: Hello, Bob\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -1619,7 +1657,6 @@ mod tests {
|
||||
\n\
|
||||
hello\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
@@ -1627,7 +1664,7 @@ mod tests {
|
||||
let new_msg = incoming_msg_to_reply_msg(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: message opened\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -1665,7 +1702,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
|
||||
@@ -1702,7 +1739,7 @@ mod tests {
|
||||
format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Different subject\n\
|
||||
In-Reply-To: {}\n\
|
||||
Message-ID: <2893@example.com>\n\
|
||||
@@ -1713,7 +1750,6 @@ mod tests {
|
||||
)
|
||||
.as_bytes(),
|
||||
"INBOX",
|
||||
5,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -1757,7 +1793,7 @@ mod tests {
|
||||
mf.subject_str(&t).await.unwrap()
|
||||
}
|
||||
|
||||
// In `imf_raw`, From has to be bob@example.com, To has to be alice@example.com
|
||||
// In `imf_raw`, From has to be bob@example.com, To has to be alice@example.org
|
||||
async fn msg_to_subject_str(imf_raw: &[u8]) -> String {
|
||||
let subject_str = msg_to_subject_str_inner(imf_raw, false, false, false).await;
|
||||
|
||||
@@ -1818,14 +1854,13 @@ mod tests {
|
||||
&t,
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: Bob <bob@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Some other, completely unrelated subject\n\
|
||||
Message-ID: <3cl4@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
Some other, completely unrelated content\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
@@ -1836,7 +1871,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();
|
||||
@@ -1850,13 +1885,13 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
dc_receive_imf(context, imf_raw, "INBOX", 1, false)
|
||||
dc_receive_imf(context, imf_raw, "INBOX", false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
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);
|
||||
@@ -1878,7 +1913,7 @@ mod tests {
|
||||
let msg = incoming_msg_to_reply_msg(
|
||||
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: Charlie <charlie@example.com>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Chat: hello\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <2223@example.com>\n\
|
||||
@@ -1896,7 +1931,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()
|
||||
@@ -1906,7 +1941,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();
|
||||
}
|
||||
@@ -1980,8 +2015,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();
|
||||
@@ -1999,8 +2035,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();
|
||||
|
||||
@@ -2017,4 +2053,35 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that removed member address does not go into the `To:` field.
|
||||
#[async_std::test]
|
||||
async fn test_remove_member_bcc() -> Result<()> {
|
||||
// Alice creates a group with Bob and Claire and then removes Bob.
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "Claire", "claire@foo.de").await?;
|
||||
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "Creating a group".to_string()).await?;
|
||||
|
||||
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
let remove = alice.pop_sent_msg().await;
|
||||
let remove_payload = remove.payload();
|
||||
let parsed = mailparse::parse_mail(remove_payload.as_bytes())?;
|
||||
let to = parsed
|
||||
.headers
|
||||
.get_first_header("To")
|
||||
.context("no To: header parsed")?;
|
||||
let to = addrparse_header(to)?;
|
||||
let mailbox = to
|
||||
.extract_single_info()
|
||||
.context("to: field does not contain exactly one address")?;
|
||||
assert_eq!(mailbox.addr, "bob@example.net");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::blob::BlobObject;
|
||||
use crate::constants::{Viewtype, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
|
||||
use crate::contact::addr_normalize;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_get_filemeta, dc_truncate};
|
||||
use crate::dc_tools::{dc_get_filemeta, dc_truncate, parse_receive_headers};
|
||||
use crate::dehtml::dehtml;
|
||||
use crate::e2ee;
|
||||
use crate::events::EventType;
|
||||
@@ -47,6 +47,7 @@ pub struct MimeMessage {
|
||||
/// Addresses are normalized and lowercased:
|
||||
pub recipients: Vec<SingleInfo>,
|
||||
pub from: Vec<SingleInfo>,
|
||||
pub list_post: Option<String>,
|
||||
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||
pub decrypting_failed: bool,
|
||||
|
||||
@@ -65,6 +66,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>,
|
||||
@@ -82,6 +84,8 @@ pub struct MimeMessage {
|
||||
/// This is non-empty only if the message was actually encrypted. It is used
|
||||
/// for e.g. late-parsing HTML.
|
||||
pub decoded_data: Vec<u8>,
|
||||
|
||||
pub(crate) hop_info: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -132,6 +136,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 {
|
||||
@@ -163,10 +171,12 @@ impl MimeMessage {
|
||||
.get_header_value(HeaderDef::Date)
|
||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
||||
.unwrap_or_default();
|
||||
let hop_info = parse_receive_headers(&mail.get_headers());
|
||||
|
||||
let mut headers = Default::default();
|
||||
let mut recipients = Default::default();
|
||||
let mut from = Default::default();
|
||||
let mut list_post = Default::default();
|
||||
let mut chat_disposition_notification_to = None;
|
||||
|
||||
// Parse IMF headers.
|
||||
@@ -175,6 +185,7 @@ impl MimeMessage {
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
&mail.headers,
|
||||
);
|
||||
@@ -248,6 +259,7 @@ impl MimeMessage {
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut throwaway_from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
&decrypted_mail.headers,
|
||||
);
|
||||
@@ -275,6 +287,7 @@ impl MimeMessage {
|
||||
parts: Vec::new(),
|
||||
header: headers,
|
||||
recipients,
|
||||
list_post,
|
||||
from,
|
||||
chat_disposition_notification_to,
|
||||
decrypting_failed: false,
|
||||
@@ -288,12 +301,14 @@ impl MimeMessage {
|
||||
location_kml: None,
|
||||
message_kml: None,
|
||||
sync_items: None,
|
||||
webxdc_status_update: None,
|
||||
user_avatar: None,
|
||||
group_avatar: None,
|
||||
failure_report: None,
|
||||
footer: None,
|
||||
is_mime_modified: false,
|
||||
decoded_data: Vec::new(),
|
||||
hop_info,
|
||||
};
|
||||
|
||||
match partial {
|
||||
@@ -388,16 +403,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);
|
||||
@@ -529,7 +546,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();
|
||||
}
|
||||
}
|
||||
@@ -828,6 +845,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
|
||||
@@ -997,8 +1020,13 @@ impl MimeMessage {
|
||||
if decoded_data.is_empty() {
|
||||
return;
|
||||
}
|
||||
// treat location/message kml file attachments specially
|
||||
if filename.ends_with(".kml") {
|
||||
let msg_type = if context
|
||||
.is_webxdc_file(filename, decoded_data)
|
||||
.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") {
|
||||
@@ -1014,6 +1042,7 @@ impl MimeMessage {
|
||||
}
|
||||
return;
|
||||
}
|
||||
msg_type
|
||||
} else if filename == "multi-device-sync.json" {
|
||||
let serialized = String::from_utf8_lossy(decoded_data)
|
||||
.parse()
|
||||
@@ -1026,7 +1055,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 */
|
||||
@@ -1092,11 +1129,11 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
|
||||
pub fn repl_msg_by_error(&mut self, error_msg: &str) {
|
||||
self.is_system_message = SystemMessage::Unknown;
|
||||
if let Some(part) = self.parts.first_mut() {
|
||||
part.typ = Viewtype::Text;
|
||||
part.msg = format!("[{}]", error_msg.as_ref());
|
||||
part.msg = format!("[{}]", error_msg);
|
||||
self.parts.truncate(1);
|
||||
}
|
||||
}
|
||||
@@ -1112,6 +1149,7 @@ impl MimeMessage {
|
||||
headers: &mut HashMap<String, String>,
|
||||
recipients: &mut Vec<SingleInfo>,
|
||||
from: &mut Vec<SingleInfo>,
|
||||
list_post: &mut Option<String>,
|
||||
chat_disposition_notification_to: &mut Option<SingleInfo>,
|
||||
fields: &[mailparse::MailHeader<'_>],
|
||||
) {
|
||||
@@ -1142,6 +1180,10 @@ impl MimeMessage {
|
||||
if !from_new.is_empty() {
|
||||
*from = from_new;
|
||||
}
|
||||
let list_post_new = get_list_post(fields);
|
||||
if list_post_new.is_some() {
|
||||
*list_post = list_post_new;
|
||||
}
|
||||
}
|
||||
|
||||
fn process_report(
|
||||
@@ -1159,23 +1201,21 @@ impl MimeMessage {
|
||||
|
||||
// must be present
|
||||
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
|
||||
if let Some(original_message_id) = report_fields
|
||||
let original_message_id = report_fields
|
||||
.get_header_value(HeaderDef::OriginalMessageId)
|
||||
.and_then(|v| parse_message_id(&v).ok())
|
||||
{
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
.map_or_else(Vec::new, |v| {
|
||||
v.split(' ')
|
||||
.filter_map(|s| parse_message_id(s).ok())
|
||||
.collect()
|
||||
});
|
||||
.and_then(|v| parse_message_id(&v).ok());
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
.map_or_else(Vec::new, |v| {
|
||||
v.split(' ')
|
||||
.filter_map(|s| parse_message_id(s).ok())
|
||||
.collect()
|
||||
});
|
||||
|
||||
return Ok(Some(Report {
|
||||
original_message_id,
|
||||
additional_message_ids,
|
||||
}));
|
||||
}
|
||||
return Ok(Some(Report {
|
||||
original_message_id,
|
||||
additional_message_ids,
|
||||
}));
|
||||
}
|
||||
warn!(
|
||||
context,
|
||||
@@ -1331,8 +1371,10 @@ impl MimeMessage {
|
||||
parts: &[Part],
|
||||
) {
|
||||
for report in &self.mdn_reports {
|
||||
for original_message_id in
|
||||
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
|
||||
for original_message_id in report
|
||||
.original_message_id
|
||||
.iter()
|
||||
.chain(&report.additional_message_ids)
|
||||
{
|
||||
match message::handle_mdn(context, from_id, original_message_id, sent_timestamp)
|
||||
.await
|
||||
@@ -1437,7 +1479,10 @@ async fn update_gossip_peerstates(
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Report {
|
||||
/// Original-Message-ID header
|
||||
original_message_id: String,
|
||||
///
|
||||
/// 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`.
|
||||
original_message_id: Option<String>,
|
||||
/// Additional-Message-IDs
|
||||
additional_message_ids: Vec<String>,
|
||||
}
|
||||
@@ -1627,6 +1672,14 @@ pub(crate) fn get_from(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||
get_all_addresses_from_header(headers, |header_key| header_key == "from")
|
||||
}
|
||||
|
||||
/// Returned addresses are normalized and lowercased.
|
||||
pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
|
||||
get_all_addresses_from_header(headers, |header_key| header_key == "list-post")
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|s| s.addr)
|
||||
}
|
||||
|
||||
fn get_all_addresses_from_header<F>(headers: &[MailHeader], pred: F) -> Vec<SingleInfo>
|
||||
where
|
||||
F: Fn(String) -> bool,
|
||||
@@ -2346,7 +2399,7 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
|
||||
assert_eq!(message.mdn_reports.len(), 1);
|
||||
assert_eq!(
|
||||
message.mdn_reports[0].original_message_id,
|
||||
"foo@example.org"
|
||||
Some("foo@example.org".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
&message.mdn_reports[0].additional_message_ids,
|
||||
@@ -2848,7 +2901,6 @@ On 2020-10-25, Bob wrote:
|
||||
&t.ctx,
|
||||
include_bytes!("../test-data/message/subj_with_multimedia_msg.eml"),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
@@ -2997,7 +3049,7 @@ Subject: ...
|
||||
|
||||
Some quote.
|
||||
"###;
|
||||
dc_receive_imf(&t, raw, "INBOX", 1, false).await?;
|
||||
dc_receive_imf(&t, raw, "INBOX", false).await?;
|
||||
|
||||
// Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long.
|
||||
let raw = br###"In-Reply-To:
|
||||
@@ -3014,7 +3066,7 @@ Subject: ...
|
||||
Some reply
|
||||
"###;
|
||||
|
||||
dc_receive_imf(&t, raw, "INBOX", 2, false).await?;
|
||||
dc_receive_imf(&t, raw, "INBOX", false).await?;
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.get_text().unwrap(), "Some reply");
|
||||
@@ -3034,21 +3086,21 @@ Some reply
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <foobarbaz@example.org>
|
||||
To: Bob <bob@example.org>
|
||||
From: Alice <alice@example.com>
|
||||
From: Alice <alice@example.org>
|
||||
Subject: subject
|
||||
Chat-Disposition-Notification-To: alice@example.com
|
||||
Chat-Disposition-Notification-To: alice@example.org
|
||||
|
||||
Message.
|
||||
"###;
|
||||
|
||||
// Bob receives message.
|
||||
dc_receive_imf(&bob, raw, "INBOX", 1, false).await?;
|
||||
dc_receive_imf(&bob, raw, "INBOX", false).await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
// Message is incoming.
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap());
|
||||
|
||||
// Alice receives copy-to-self.
|
||||
dc_receive_imf(&alice, raw, "INBOX", 1, false).await?;
|
||||
dc_receive_imf(&alice, raw, "INBOX", false).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
// Message is outgoing, don't send read receipt to self.
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).is_none());
|
||||
@@ -3064,18 +3116,17 @@ Message.
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: alice@example.com\n\
|
||||
From: alice@example.org\n\
|
||||
To: bob@example.net\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: first@example.com\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: alice@example.com\n\
|
||||
Chat-Disposition-Notification-To: alice@example.org\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
.as_bytes(),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -3087,8 +3138,8 @@ Message.
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: alice@example.com\n\
|
||||
To: alice@example.com\n\
|
||||
From: alice@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: message opened\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -3114,7 +3165,6 @@ Message.
|
||||
--SNIPP--"
|
||||
.as_bytes(),
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -3125,4 +3175,18 @@ Message.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test parsing of MDN sent by MS Exchange.
|
||||
///
|
||||
/// It does not have required Original-Message-ID field, so it is useless, but we want to
|
||||
/// recognize it as MDN nevertheless to avoid displaying it in the chat as normal message.
|
||||
#[async_std::test]
|
||||
async fn test_ms_exchange_mdn() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let raw =
|
||||
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());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ struct Oauth2 {
|
||||
|
||||
/// OAuth 2 Access Token Response
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct Response {
|
||||
// Should always be there according to: <https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/>
|
||||
// but previous code handled its abscense.
|
||||
@@ -58,7 +59,7 @@ pub async fn dc_get_oauth2_url(
|
||||
redirect_uri: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
|
||||
if let Some(oauth2) = Oauth2::from_address(addr, socks5_enabled).await {
|
||||
if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await {
|
||||
context
|
||||
.sql
|
||||
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri))
|
||||
@@ -79,7 +80,7 @@ pub async fn dc_get_oauth2_access_token(
|
||||
regenerate: bool,
|
||||
) -> Result<Option<String>> {
|
||||
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
|
||||
if let Some(oauth2) = Oauth2::from_address(addr, socks5_enabled).await {
|
||||
if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await {
|
||||
let lock = context.oauth2_mutex.lock().await;
|
||||
|
||||
// read generated token
|
||||
@@ -225,7 +226,7 @@ pub async fn dc_get_oauth2_addr(
|
||||
code: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
|
||||
let oauth2 = match Oauth2::from_address(addr, socks5_enabled).await {
|
||||
let oauth2 = match Oauth2::from_address(context, addr, socks5_enabled).await {
|
||||
Some(o) => o,
|
||||
None => return Ok(None),
|
||||
};
|
||||
@@ -253,13 +254,13 @@ pub async fn dc_get_oauth2_addr(
|
||||
}
|
||||
|
||||
impl Oauth2 {
|
||||
async fn from_address(addr: &str, skip_mx: bool) -> Option<Self> {
|
||||
async fn from_address(context: &Context, addr: &str, skip_mx: bool) -> Option<Self> {
|
||||
let addr_normalized = normalize_addr(addr);
|
||||
if let Some(domain) = addr_normalized
|
||||
.find('@')
|
||||
.map(|index| addr_normalized.split_at(index + 1).1)
|
||||
{
|
||||
if let Some(oauth2_authorizer) = provider::get_provider_info(domain, skip_mx)
|
||||
if let Some(oauth2_authorizer) = provider::get_provider_info(context, domain, skip_mx)
|
||||
.await
|
||||
.and_then(|provider| provider.oauth2_authorizer.as_ref())
|
||||
{
|
||||
@@ -356,32 +357,39 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_oauth_from_address() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
Oauth2::from_address("hello@gmail.com", false).await,
|
||||
Oauth2::from_address(&t, "hello@gmail.com", false).await,
|
||||
Some(OAUTH2_GMAIL)
|
||||
);
|
||||
assert_eq!(
|
||||
Oauth2::from_address("hello@googlemail.com", false).await,
|
||||
Oauth2::from_address(&t, "hello@googlemail.com", false).await,
|
||||
Some(OAUTH2_GMAIL)
|
||||
);
|
||||
assert_eq!(
|
||||
Oauth2::from_address("hello@yandex.com", false).await,
|
||||
Oauth2::from_address(&t, "hello@yandex.com", false).await,
|
||||
Some(OAUTH2_YANDEX)
|
||||
);
|
||||
assert_eq!(
|
||||
Oauth2::from_address("hello@yandex.ru", false).await,
|
||||
Oauth2::from_address(&t, "hello@yandex.ru", false).await,
|
||||
Some(OAUTH2_YANDEX)
|
||||
);
|
||||
|
||||
assert_eq!(Oauth2::from_address("hello@web.de", false).await, None);
|
||||
assert_eq!(Oauth2::from_address(&t, "hello@web.de", false).await, None);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_oauth_from_mx() {
|
||||
// youtube staff seems to use "google workspace with oauth2", figures this out by MX lookup
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
Oauth2::from_address("hello@google.com", false).await,
|
||||
Oauth2::from_address(&t, "hello@youtube.com", false).await,
|
||||
Some(OAUTH2_GMAIL)
|
||||
);
|
||||
// without MX lookup, we would not know as youtube.com is not in our provider-db
|
||||
assert_eq!(
|
||||
Oauth2::from_address(&t, "hello@youtube.com", true).await,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
|
||||
45
src/param.rs
45
src/param.rs
@@ -4,7 +4,6 @@ use std::str;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use async_std::path::PathBuf;
|
||||
use itertools::Itertools;
|
||||
use num_traits::FromPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -111,8 +110,8 @@ 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',
|
||||
|
||||
/// For Groups
|
||||
///
|
||||
@@ -137,8 +136,17 @@ pub enum Param {
|
||||
/// For Chats
|
||||
Devicetalk = b'D',
|
||||
|
||||
/// For MDN-sending job
|
||||
MsgId = b'I',
|
||||
/// For Chats: If this is a mailing list chat, contains the List-Post address.
|
||||
/// None if there simply is no `List-Post` header in the mailing list.
|
||||
/// Some("") if the mailing list is using multiple different List-Post headers.
|
||||
///
|
||||
/// The List-Post address is the email address where the user can write to in order to
|
||||
/// post something to the mailing list.
|
||||
ListPost = b'p',
|
||||
|
||||
/// For Contacts: If this is the List-Post address of a mailing list, contains
|
||||
/// the List-Id of the mailing list (which is also used as the group id of the chat).
|
||||
ListId = b's',
|
||||
|
||||
/// For Contacts: timestamp of status (aka signature or footer) update.
|
||||
StatusTimestamp = b'j',
|
||||
@@ -160,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.
|
||||
@@ -182,7 +196,7 @@ impl fmt::Display for Params {
|
||||
f,
|
||||
"{}={}",
|
||||
*key as u8 as char,
|
||||
value.split('\n').join("\n\n")
|
||||
value.split('\n').collect::<Vec<&str>>().join("\n\n")
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -192,6 +206,11 @@ impl fmt::Display for Params {
|
||||
impl str::FromStr for Params {
|
||||
type Err = Error;
|
||||
|
||||
/// Parse a raw string to Param.
|
||||
///
|
||||
/// Silently ignore unknown keys:
|
||||
/// they may come from a downgrade (when a shortly new version adds a key)
|
||||
/// or from an upgrade (when a key is dropped but was used in the past)
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let mut inner = BTreeMap::new();
|
||||
let mut lines = s.lines().peekable();
|
||||
@@ -211,8 +230,6 @@ impl str::FromStr for Params {
|
||||
|
||||
if let Some(key) = key.as_bytes().first().and_then(|key| Param::from_u8(*key)) {
|
||||
inner.insert(key, value);
|
||||
} else {
|
||||
bail!("Unknown key: {}", key);
|
||||
}
|
||||
} else {
|
||||
bail!("Not a key-value pair: {:?}", line);
|
||||
@@ -415,10 +432,12 @@ impl<'a> ParamsFile<'a> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_std::fs;
|
||||
use async_std::path::Path;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_dc_param() {
|
||||
@@ -521,4 +540,14 @@ mod tests {
|
||||
assert!(p.get_path(Param::File, &t).unwrap().is_none());
|
||||
assert!(p.get_blob(Param::File, &t, false).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_params_unknown_key() -> Result<()> {
|
||||
// 'Z' is used as a key that is known to be unused; these keys should be ignored silently by definition.
|
||||
let p = Params::from_str("w=12\nZ=13\nh=14")?;
|
||||
assert_eq!(p.len(), 2);
|
||||
assert_eq!(p.get(Param::Width), Some("12"));
|
||||
assert_eq!(p.get(Param::Height), Some("14"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ impl Peerstate {
|
||||
|
||||
let msg = stock_str::contact_setup_changed(context, self.addr.clone()).await;
|
||||
|
||||
chat::add_info_msg(context, chat_id, msg, timestamp).await?;
|
||||
chat::add_info_msg(context, chat_id, &msg, timestamp).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
} else {
|
||||
bail!("contact with peerstate.addr {:?} not found", &self.addr);
|
||||
@@ -524,7 +524,6 @@ pub(crate) async fn deduplicate_peerstates(sql: &Sql) -> Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::alice_keypair;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_peerstate_save_to_db() {
|
||||
|
||||
129
src/pgp.rs
129
src/pgp.rs
@@ -4,11 +4,11 @@ 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,
|
||||
SignedPublicSubKey, SignedSecretKey, SubkeyParamsBuilder,
|
||||
SignedPublicSubKey, SignedSecretKey, StandaloneSignature, SubkeyParamsBuilder,
|
||||
};
|
||||
use pgp::crypto::{HashAlgorithm, SymmetricKeyAlgorithm};
|
||||
use pgp::types::{
|
||||
@@ -248,7 +248,7 @@ pub async fn pk_encrypt(
|
||||
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
|
||||
.keys()
|
||||
.iter()
|
||||
.filter_map(|key| select_pk_for_encryption(key))
|
||||
.filter_map(select_pk_for_encryption)
|
||||
.collect();
|
||||
let pkeys_refs: Vec<&SignedPublicKeyOrSubkey> = pkeys.iter().collect();
|
||||
|
||||
@@ -277,16 +277,17 @@ pub async fn pk_encrypt(
|
||||
/// Receiver private keys are provided in
|
||||
/// `private_keys_for_decryption`.
|
||||
///
|
||||
/// If `ret_signature_fingerprints` is not `None`, stores fingerprints
|
||||
/// Returns decrypted message and fingerprints
|
||||
/// of all keys from the `public_keys_for_validation` keyring that
|
||||
/// have valid signatures there.
|
||||
#[allow(clippy::implicit_hasher)]
|
||||
pub async fn pk_decrypt(
|
||||
ctext: Vec<u8>,
|
||||
private_keys_for_decryption: Keyring<SignedSecretKey>,
|
||||
public_keys_for_validation: Keyring<SignedPublicKey>,
|
||||
ret_signature_fingerprints: Option<&mut HashSet<Fingerprint>>,
|
||||
) -> Result<Vec<u8>> {
|
||||
public_keys_for_validation: &Keyring<SignedPublicKey>,
|
||||
) -> Result<(Vec<u8>, HashSet<Fingerprint>)> {
|
||||
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
|
||||
|
||||
let msgs = async_std::task::spawn_blocking(move || {
|
||||
let cursor = Cursor::new(ctext);
|
||||
let (msg, _) = Message::from_armor_single(cursor)?;
|
||||
@@ -308,33 +309,54 @@ pub async fn pk_decrypt(
|
||||
None => bail!("The decrypted message is empty"),
|
||||
};
|
||||
|
||||
if let Some(ret_signature_fingerprints) = ret_signature_fingerprints {
|
||||
if !public_keys_for_validation.is_empty() {
|
||||
let fingerprints = async_std::task::spawn_blocking(move || {
|
||||
let pkeys = public_keys_for_validation.keys();
|
||||
if !public_keys_for_validation.is_empty() {
|
||||
let pkeys = public_keys_for_validation.keys();
|
||||
|
||||
let mut fingerprints: Vec<Fingerprint> = Vec::new();
|
||||
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
|
||||
for pkey in pkeys {
|
||||
if signed_msg.verify(&pkey.primary_key).is_ok() {
|
||||
let fp = DcKey::fingerprint(pkey);
|
||||
fingerprints.push(fp);
|
||||
}
|
||||
}
|
||||
let mut fingerprints: Vec<Fingerprint> = Vec::new();
|
||||
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
|
||||
for pkey in pkeys {
|
||||
if signed_msg.verify(&pkey.primary_key).is_ok() {
|
||||
let fp = DcKey::fingerprint(pkey);
|
||||
fingerprints.push(fp);
|
||||
}
|
||||
fingerprints
|
||||
})
|
||||
.await;
|
||||
|
||||
ret_signature_fingerprints.extend(fingerprints);
|
||||
}
|
||||
}
|
||||
|
||||
ret_signature_fingerprints.extend(fingerprints);
|
||||
}
|
||||
Ok(content)
|
||||
Ok((content, ret_signature_fingerprints))
|
||||
} else {
|
||||
bail!("No valid messages found");
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates detached signature.
|
||||
pub async fn pk_validate(
|
||||
content: &[u8],
|
||||
signature: &[u8],
|
||||
public_keys_for_validation: &Keyring<SignedPublicKey>,
|
||||
) -> Result<HashSet<Fingerprint>> {
|
||||
let mut ret: HashSet<Fingerprint> = Default::default();
|
||||
|
||||
let standalone_signature = StandaloneSignature::from_armor_single(Cursor::new(signature))?.0;
|
||||
let pkeys = public_keys_for_validation.keys();
|
||||
|
||||
// Remove trailing CRLF before the delimiter.
|
||||
// According to RFC 3156 it is considered to be part of the MIME delimiter for the purpose of
|
||||
// OpenPGP signature calculation.
|
||||
let content = content
|
||||
.get(..content.len().saturating_sub(2))
|
||||
.context("index is out of range")?;
|
||||
|
||||
for pkey in pkeys {
|
||||
if standalone_signature.verify(pkey, content).is_ok() {
|
||||
let fp = DcKey::fingerprint(pkey);
|
||||
ret.insert(fp);
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Symmetric encryption.
|
||||
pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
|
||||
let lit_msg = Message::new_literal_bytes("", plain);
|
||||
@@ -492,15 +514,12 @@ mod tests {
|
||||
decrypt_keyring.add(KEYS.alice_secret.clone());
|
||||
let mut sig_check_keyring: Keyring<SignedPublicKey> = Keyring::new();
|
||||
sig_check_keyring.add(KEYS.alice_public.clone());
|
||||
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes().to_vec(),
|
||||
decrypt_keyring,
|
||||
sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
&sig_check_keyring,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| println!("{:?}", err))
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 1);
|
||||
@@ -510,15 +529,12 @@ mod tests {
|
||||
decrypt_keyring.add(KEYS.bob_secret.clone());
|
||||
let mut sig_check_keyring = Keyring::new();
|
||||
sig_check_keyring.add(KEYS.alice_public.clone());
|
||||
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes().to_vec(),
|
||||
decrypt_keyring,
|
||||
sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
&sig_check_keyring,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| println!("{:?}", err))
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 1);
|
||||
@@ -529,15 +545,10 @@ mod tests {
|
||||
let mut keyring = Keyring::new();
|
||||
keyring.add(KEYS.alice_secret.clone());
|
||||
let empty_keyring = Keyring::new();
|
||||
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes().to_vec(),
|
||||
keyring,
|
||||
empty_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let (plain, valid_signatures) =
|
||||
pk_decrypt(CTEXT_SIGNED.as_bytes().to_vec(), keyring, &empty_keyring)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
@@ -549,12 +560,10 @@ mod tests {
|
||||
decrypt_keyring.add(KEYS.bob_secret.clone());
|
||||
let mut sig_check_keyring = Keyring::new();
|
||||
sig_check_keyring.add(KEYS.bob_public.clone());
|
||||
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes().to_vec(),
|
||||
decrypt_keyring,
|
||||
sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
&sig_check_keyring,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -567,34 +576,14 @@ mod tests {
|
||||
let mut decrypt_keyring = Keyring::new();
|
||||
decrypt_keyring.add(KEYS.bob_secret.clone());
|
||||
let sig_check_keyring = Keyring::new();
|
||||
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
CTEXT_UNSIGNED.as_bytes().to_vec(),
|
||||
decrypt_keyring,
|
||||
sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
&sig_check_keyring,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_decrypt_signed_no_sigret() {
|
||||
// Check decrypting signed cyphertext without providing the HashSet for signatures.
|
||||
let mut decrypt_keyring = Keyring::new();
|
||||
decrypt_keyring.add(KEYS.bob_secret.clone());
|
||||
let mut sig_check_keyring = Keyring::new();
|
||||
sig_check_keyring.add(KEYS.alice_public.clone());
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes().to_vec(),
|
||||
decrypt_keyring,
|
||||
sig_check_keyring,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
mod data;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS, PROVIDER_UPDATED};
|
||||
use async_std_resolver::resolver_from_system_conf;
|
||||
use anyhow::Result;
|
||||
use async_std_resolver::{config, resolver, resolver_from_system_conf, AsyncStdResolver};
|
||||
use chrono::{NaiveDateTime, NaiveTime};
|
||||
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
|
||||
@@ -81,6 +83,23 @@ pub struct Provider {
|
||||
pub oauth2_authorizer: Option<Oauth2Authorizer>,
|
||||
}
|
||||
|
||||
/// Get resolver to query MX records.
|
||||
///
|
||||
/// We first try resolver_from_system_conf() which reads the system's resolver from `/etc/resolv.conf`.
|
||||
/// This does not work at least on some Androids, therefore we use use ResolverConfig::default()
|
||||
/// which default eg. to google's 8.8.8.8 or 8.8.4.4 as a fallback.
|
||||
async fn get_resolver() -> Result<AsyncStdResolver> {
|
||||
if let Ok(resolver) = resolver_from_system_conf().await {
|
||||
return Ok(resolver);
|
||||
}
|
||||
let resolver = resolver(
|
||||
config::ResolverConfig::default(),
|
||||
config::ResolverOpts::default(),
|
||||
)
|
||||
.await?;
|
||||
Ok(resolver)
|
||||
}
|
||||
|
||||
/// Returns provider for the given domain.
|
||||
///
|
||||
/// This function looks up domain in offline database first. If not
|
||||
@@ -89,7 +108,11 @@ pub struct Provider {
|
||||
///
|
||||
/// For compatibility, email address can be passed to this function
|
||||
/// instead of the domain.
|
||||
pub async fn get_provider_info(domain: &str, skip_mx: bool) -> Option<&'static Provider> {
|
||||
pub async fn get_provider_info(
|
||||
context: &Context,
|
||||
domain: &str,
|
||||
skip_mx: bool,
|
||||
) -> Option<&'static Provider> {
|
||||
let domain = domain.rsplitn(2, '@').next()?;
|
||||
|
||||
if let Some(provider) = get_provider_by_domain(domain) {
|
||||
@@ -97,7 +120,7 @@ pub async fn get_provider_info(domain: &str, skip_mx: bool) -> Option<&'static P
|
||||
}
|
||||
|
||||
if !skip_mx {
|
||||
if let Some(provider) = get_provider_by_mx(domain).await {
|
||||
if let Some(provider) = get_provider_by_mx(context, domain).await {
|
||||
return Some(provider);
|
||||
}
|
||||
}
|
||||
@@ -117,8 +140,8 @@ pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> {
|
||||
/// Finds a provider based on MX record for the given domain.
|
||||
///
|
||||
/// For security reasons, only Gmail can be configured this way.
|
||||
pub async fn get_provider_by_mx(domain: &str) -> Option<&'static Provider> {
|
||||
if let Ok(resolver) = resolver_from_system_conf().await {
|
||||
pub async fn get_provider_by_mx(context: &Context, domain: &str) -> Option<&'static Provider> {
|
||||
if let Ok(resolver) = get_resolver().await {
|
||||
let mut fqdn: String = domain.to_string();
|
||||
if !fqdn.ends_with('.') {
|
||||
fqdn.push('.');
|
||||
@@ -143,6 +166,8 @@ pub async fn get_provider_by_mx(domain: &str) -> Option<&'static Provider> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(context, "cannot get a resolver to check MX records.");
|
||||
}
|
||||
|
||||
None
|
||||
@@ -169,6 +194,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::dc_tools::time;
|
||||
use crate::test_utils::TestContext;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
#[test]
|
||||
@@ -218,12 +244,13 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_provider_info() {
|
||||
assert!(get_provider_info("", false).await.is_none());
|
||||
assert!(get_provider_info("google.com", false).await.unwrap().id == "gmail");
|
||||
let t = TestContext::new().await;
|
||||
assert!(get_provider_info(&t, "", false).await.is_none());
|
||||
assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail");
|
||||
|
||||
// get_provider_info() accepts email addresses for backwards compatibility
|
||||
assert!(
|
||||
get_provider_info("example@google.com", false)
|
||||
get_provider_info(&t, "example@google.com", false)
|
||||
.await
|
||||
.unwrap()
|
||||
.id
|
||||
@@ -242,4 +269,10 @@ mod tests {
|
||||
assert!(get_provider_update_timestamp() <= time());
|
||||
assert!(get_provider_update_timestamp() > timestamp_past);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_resolver() -> Result<()> {
|
||||
assert!(get_resolver().await.is_ok());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,61 +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::MvboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
},
|
||||
]),
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
oauth2_authorizer: None,
|
||||
@@ -404,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,
|
||||
@@ -427,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,
|
||||
@@ -475,10 +430,6 @@ static P_FIVE_CHAT: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
key: Config::SentboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
@@ -668,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",
|
||||
@@ -696,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![
|
||||
@@ -751,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,
|
||||
@@ -823,10 +847,6 @@ static P_NAUTA_CU: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
key: Config::SentboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
@@ -878,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,
|
||||
@@ -907,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,
|
||||
@@ -936,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",
|
||||
@@ -979,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,
|
||||
@@ -1036,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,
|
||||
@@ -1101,10 +1151,6 @@ static P_TESTRUN: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
key: Config::SentboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxWatch,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
@@ -1437,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),
|
||||
@@ -1467,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),
|
||||
@@ -1485,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),
|
||||
@@ -1537,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),
|
||||
@@ -1633,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),
|
||||
@@ -1648,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),
|
||||
@@ -1686,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));
|
||||
|
||||
20
src/qr.rs
20
src/qr.rs
@@ -282,7 +282,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
chat.id,
|
||||
format!("{} verified.", peerstate.addr),
|
||||
&format!("{} verified.", peerstate.addr),
|
||||
time(),
|
||||
)
|
||||
.await?;
|
||||
@@ -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(),
|
||||
})
|
||||
@@ -424,7 +424,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
grpid,
|
||||
..
|
||||
} => {
|
||||
let chat_id = get_chat_id_by_grpid(context, grpid)
|
||||
let chat_id = get_chat_id_by_grpid(context, &grpid)
|
||||
.await?
|
||||
.map(|(chat_id, _protected, _blocked)| chat_id);
|
||||
token::save(
|
||||
@@ -792,12 +792,12 @@ mod tests {
|
||||
async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.com")
|
||||
let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.org")
|
||||
.await
|
||||
.context("failed to create contact")?;
|
||||
let pub_key = alice_keypair().public;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.com".to_string(),
|
||||
addr: "alice@example.org".to_string(),
|
||||
last_seen: 1,
|
||||
last_seen_autocrypt: 1,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
@@ -818,7 +818,7 @@ mod tests {
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.com",
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprMismatch { contact_id, .. } = qr {
|
||||
@@ -829,7 +829,7 @@ mod tests {
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
&format!("OPENPGP4FPR:{}#a=alice@example.com", pub_key.fingerprint()),
|
||||
&format!("OPENPGP4FPR:{}#a=alice@example.org", pub_key.fingerprint()),
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprOk { contact_id, .. } = qr {
|
||||
|
||||
265
src/qr_code_generator.rs
Normal file
265
src/qr_code_generator.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
use anyhow::Result;
|
||||
use qrcodegen::{QrCode, QrCodeEcc};
|
||||
|
||||
use crate::{
|
||||
blob::BlobObject,
|
||||
chat::{Chat, ChatId},
|
||||
color::color_int_to_hex_string,
|
||||
config::Config,
|
||||
constants::DC_CONTACT_ID_SELF,
|
||||
contact::Contact,
|
||||
context::Context,
|
||||
securejoin, stock_str,
|
||||
};
|
||||
|
||||
pub async fn get_securejoin_qr_svg(context: &Context, chat_id: Option<ChatId>) -> Result<String> {
|
||||
if let Some(chat_id) = chat_id {
|
||||
generate_join_group_qr_code(context, chat_id).await
|
||||
} else {
|
||||
generate_verification_qr(context).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_join_group_qr_code(context: &Context, chat_id: ChatId) -> Result<String> {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
let avatar = match chat.get_profile_image(context).await? {
|
||||
Some(path) => {
|
||||
let avatar_blob = BlobObject::from_path(context, &path)?;
|
||||
Some(std::fs::read(avatar_blob.to_abs_path())?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
inner_generate_secure_join_qr_code(
|
||||
&stock_str::secure_join_group_qr_description(context, &chat).await,
|
||||
&securejoin::dc_get_securejoin_qr(context, Some(chat_id)).await?,
|
||||
&color_int_to_hex_string(chat.get_color(context).await?),
|
||||
avatar,
|
||||
chat.get_name().chars().next().unwrap_or('#'),
|
||||
)
|
||||
}
|
||||
|
||||
async fn generate_verification_qr(context: &Context) -> Result<String> {
|
||||
let contact = Contact::get_by_id(context, DC_CONTACT_ID_SELF).await?;
|
||||
|
||||
let avatar = match contact.get_profile_image(context).await? {
|
||||
Some(path) => {
|
||||
let avatar_blob = BlobObject::from_path(context, &path)?;
|
||||
Some(std::fs::read(avatar_blob.to_abs_path())?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let displayname = match context.get_config(Config::Displayname).await? {
|
||||
Some(name) => name,
|
||||
None => contact.get_addr().to_owned(),
|
||||
};
|
||||
|
||||
inner_generate_secure_join_qr_code(
|
||||
&stock_str::setup_contact_qr_description(context, &displayname, contact.get_addr()).await,
|
||||
&securejoin::dc_get_securejoin_qr(context, None).await?,
|
||||
&color_int_to_hex_string(contact.get_color()),
|
||||
avatar,
|
||||
displayname.chars().next().unwrap_or('#'),
|
||||
)
|
||||
}
|
||||
|
||||
fn inner_generate_secure_join_qr_code(
|
||||
raw_qrcode_description: &str,
|
||||
qrcode_content: &str,
|
||||
color: &str,
|
||||
avatar: Option<Vec<u8>>,
|
||||
avatar_letter: char,
|
||||
) -> Result<String> {
|
||||
let qrcode_description = &escaper::encode_minimal(raw_qrcode_description);
|
||||
// config
|
||||
let width = 515.0;
|
||||
let height = 630.0;
|
||||
let logo_offset = 28.0;
|
||||
let qr_code_size = 400.0;
|
||||
let qr_translate_up = 40.0;
|
||||
let text_y_pos = ((height - qr_code_size) / 2.0) + qr_code_size;
|
||||
let avatar_border_size = 9.0;
|
||||
let card_border_size = 2.0;
|
||||
let card_roundness = 40.0;
|
||||
|
||||
let qr = QrCode::encode_text(qrcode_content, QrCodeEcc::Medium)?;
|
||||
let mut svg = String::with_capacity(28000);
|
||||
let mut w = tagger::new(&mut svg);
|
||||
|
||||
w.elem("svg", |d| {
|
||||
d.attr("xmlns", "http://www.w3.org/2000/svg")?;
|
||||
d.attr("viewBox", format_args!("0 0 {} {}", width, height))?;
|
||||
Ok(())
|
||||
})?
|
||||
.build(|w| {
|
||||
// White Background apears like a card
|
||||
w.single("rect", |d| {
|
||||
d.attr("x", card_border_size)?;
|
||||
d.attr("y", card_border_size)?;
|
||||
d.attr("rx", card_roundness)?;
|
||||
d.attr("stroke", "#c6c6c6")?;
|
||||
d.attr("stroke-width", card_border_size)?;
|
||||
d.attr("width", width - (card_border_size * 2.0))?;
|
||||
d.attr("height", height - (card_border_size * 2.0))?;
|
||||
d.attr("style", "fill:#f2f2f2")?;
|
||||
Ok(())
|
||||
})?;
|
||||
// Qrcode
|
||||
w.elem("g", |d| {
|
||||
d.attr(
|
||||
"transform",
|
||||
format!(
|
||||
"translate({},{})",
|
||||
(width - qr_code_size) / 2.0,
|
||||
((height - qr_code_size) / 2.0) - qr_translate_up
|
||||
),
|
||||
)
|
||||
// If the qr code should be in the wrong place,
|
||||
// we could also translate and scale the points in the path already,
|
||||
// but that would make the resulting svg way bigger in size and might bring up rounding issues,
|
||||
// so better avoid doing it manually if possible
|
||||
})?
|
||||
.build(|w| {
|
||||
w.single("path", |d| {
|
||||
let mut path_data = String::with_capacity(0);
|
||||
let scale = qr_code_size / qr.size() as f32;
|
||||
|
||||
for y in 0..qr.size() {
|
||||
for x in 0..qr.size() {
|
||||
if qr.get_module(x, y) {
|
||||
path_data += &format!("M{},{}h1v1h-1z", x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.attr("style", "fill:#000000")?;
|
||||
d.attr("d", path_data)?;
|
||||
d.attr("transform", format!("scale({})", scale))
|
||||
})
|
||||
})?;
|
||||
|
||||
// Text
|
||||
const BIG_TEXT_CHARS_PER_LINE: usize = 32;
|
||||
const SMALL_TEXT_CHARS_PER_LINE: usize = 38;
|
||||
let chars_per_line = if qrcode_description.len() > SMALL_TEXT_CHARS_PER_LINE * 2 {
|
||||
SMALL_TEXT_CHARS_PER_LINE
|
||||
} else {
|
||||
BIG_TEXT_CHARS_PER_LINE
|
||||
};
|
||||
let lines = textwrap::fill(qrcode_description, chars_per_line);
|
||||
let (text_font_size, text_y_shift) = if lines.split('\n').count() <= 2 {
|
||||
(27.0, 0.0)
|
||||
} else {
|
||||
(19.0, -10.0)
|
||||
};
|
||||
for (count, line) in lines.split('\n').enumerate() {
|
||||
w.elem("text", |d| {
|
||||
d.attr(
|
||||
"y",
|
||||
(count as f32 * (text_font_size * 1.2)) + text_y_pos + text_y_shift,
|
||||
)?;
|
||||
d.attr("x", width / 2.0)?;
|
||||
d.attr("text-anchor", "middle")?;
|
||||
d.attr(
|
||||
"style",
|
||||
format!(
|
||||
"font-family:sans-serif;\
|
||||
font-weight:bold;\
|
||||
font-size:{}px;\
|
||||
fill:#000000;\
|
||||
stroke:none",
|
||||
text_font_size
|
||||
),
|
||||
)
|
||||
})?
|
||||
.build(|w| w.put_raw(line))?;
|
||||
}
|
||||
// contact avatar in middle of qrcode
|
||||
const LOGO_SIZE: f32 = 94.4;
|
||||
const HALF_LOGO_SIZE: f32 = LOGO_SIZE / 2.0;
|
||||
let logo_position_in_qr = (qr_code_size / 2.0) - HALF_LOGO_SIZE;
|
||||
let logo_position_x = ((width - qr_code_size) / 2.0) + logo_position_in_qr;
|
||||
let logo_position_y =
|
||||
((height - qr_code_size) / 2.0) - qr_translate_up + logo_position_in_qr;
|
||||
|
||||
w.single("circle", |d| {
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)?;
|
||||
d.attr("cy", logo_position_y + HALF_LOGO_SIZE)?;
|
||||
d.attr("r", HALF_LOGO_SIZE + avatar_border_size)?;
|
||||
d.attr("style", "fill:#f2f2f2")
|
||||
})?;
|
||||
|
||||
if let Some(img) = avatar {
|
||||
w.elem("defs", tagger::no_attr())?.build(|w| {
|
||||
w.elem("clipPath", |d| d.attr("id", "avatar-cut"))?
|
||||
.build(|w| {
|
||||
w.single("circle", |d| {
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)?;
|
||||
d.attr("cy", logo_position_y + HALF_LOGO_SIZE)?;
|
||||
d.attr("r", HALF_LOGO_SIZE)
|
||||
})
|
||||
})
|
||||
})?;
|
||||
|
||||
w.single("image", |d| {
|
||||
d.attr("x", logo_position_x)?;
|
||||
d.attr("y", logo_position_y)?;
|
||||
d.attr("width", HALF_LOGO_SIZE * 2.0)?;
|
||||
d.attr("height", HALF_LOGO_SIZE * 2.0)?;
|
||||
d.attr("preserveAspectRatio", "none")?;
|
||||
d.attr("clip-path", "url(#avatar-cut)")?;
|
||||
d.attr(
|
||||
"href", /*might need xlink:href instead if it doesn't work on older devices?*/
|
||||
format!("data:image/jpeg;base64,{}", base64::encode(img)),
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
w.single("circle", |d| {
|
||||
d.attr("cx", logo_position_x + HALF_LOGO_SIZE)?;
|
||||
d.attr("cy", logo_position_y + HALF_LOGO_SIZE)?;
|
||||
d.attr("r", HALF_LOGO_SIZE)?;
|
||||
d.attr("style", format!("fill:{}", &color))
|
||||
})?;
|
||||
|
||||
let avatar_font_size = LOGO_SIZE * 0.65;
|
||||
let font_offset = avatar_font_size * 0.1;
|
||||
w.elem("text", |d| {
|
||||
d.attr("y", logo_position_y + HALF_LOGO_SIZE + font_offset)?;
|
||||
d.attr("x", logo_position_x + HALF_LOGO_SIZE)?;
|
||||
d.attr("text-anchor", "middle")?;
|
||||
d.attr("dominant-baseline", "central")?;
|
||||
d.attr("alignment-baseline", "middle")?;
|
||||
d.attr(
|
||||
"style",
|
||||
format!(
|
||||
"font-family:sans-serif;\
|
||||
font-weight:400;\
|
||||
font-size:{}px;\
|
||||
fill:#ffffff;",
|
||||
avatar_font_size
|
||||
),
|
||||
)
|
||||
})?
|
||||
.build(|w| w.put_raw(avatar_letter.to_uppercase()))?;
|
||||
}
|
||||
|
||||
// Footer logo
|
||||
const FOOTER_HEIGHT: f32 = 35.0;
|
||||
const FOOTER_WIDTH: f32 = 198.0;
|
||||
w.elem("g", |d| {
|
||||
d.attr(
|
||||
"transform",
|
||||
format!(
|
||||
"translate({},{})",
|
||||
(width - FOOTER_WIDTH) / 2.0,
|
||||
height - logo_offset - FOOTER_HEIGHT - text_y_shift
|
||||
),
|
||||
)
|
||||
})?
|
||||
.build(|w| w.put_raw(include_str!("../assets/qrcode_logo_footer.svg")))
|
||||
})?;
|
||||
|
||||
Ok(svg)
|
||||
}
|
||||
20
src/quota.rs
20
src/quota.rs
@@ -1,8 +1,8 @@
|
||||
//! # Support for IMAP QUOTA extension.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_imap::types::{Quota, QuotaResource};
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::chat::add_device_msg_with_importance;
|
||||
use crate::config::Config;
|
||||
@@ -43,7 +43,7 @@ pub struct QuotaInfo {
|
||||
/// set to `Err()` if the provider does not support quota or on other errors,
|
||||
/// set to `Ok()` for valid quota information.
|
||||
/// Updated by `Action::UpdateRecentQuota`
|
||||
pub(crate) recent: Result<IndexMap<String, Vec<QuotaResource>>>,
|
||||
pub(crate) recent: Result<BTreeMap<String, Vec<QuotaResource>>>,
|
||||
|
||||
/// Timestamp when structure was modified.
|
||||
pub(crate) modified: i64,
|
||||
@@ -52,8 +52,8 @@ pub struct QuotaInfo {
|
||||
async fn get_unique_quota_roots_and_usage(
|
||||
folders: Vec<String>,
|
||||
imap: &mut Imap,
|
||||
) -> Result<IndexMap<String, Vec<QuotaResource>>> {
|
||||
let mut unique_quota_roots: IndexMap<String, Vec<QuotaResource>> = IndexMap::new();
|
||||
) -> Result<BTreeMap<String, Vec<QuotaResource>>> {
|
||||
let mut unique_quota_roots: BTreeMap<String, Vec<QuotaResource>> = BTreeMap::new();
|
||||
for folder in folders {
|
||||
let (quota_roots, quotas) = &imap.get_quota_roots(&folder).await?;
|
||||
// if there are new quota roots found in this imap folder, add them to the list
|
||||
@@ -64,12 +64,12 @@ 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
|
||||
.entry(quota_root_name.clone())
|
||||
.or_insert(vec![]) = quota.resources;
|
||||
.or_insert_with(Vec::new) = quota.resources;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ async fn get_unique_quota_roots_and_usage(
|
||||
}
|
||||
|
||||
fn get_highest_usage<'t>(
|
||||
unique_quota_roots: &'t IndexMap<String, Vec<QuotaResource>>,
|
||||
unique_quota_roots: &'t BTreeMap<String, Vec<QuotaResource>>,
|
||||
) -> Result<(u64, &'t String, &QuotaResource)> {
|
||||
let mut highest: Option<(u64, &'t String, &QuotaResource)> = None;
|
||||
for (name, resources) in unique_quota_roots {
|
||||
@@ -96,7 +96,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.
|
||||
@@ -137,7 +137,7 @@ impl Context {
|
||||
}
|
||||
|
||||
let quota = if imap.can_check_quota() {
|
||||
let folders = get_watched_folders(self).await;
|
||||
let folders = get_watched_folders(self).await?;
|
||||
get_unique_quota_roots_and_usage(folders, imap).await
|
||||
} else {
|
||||
Err(anyhow!(stock_str::not_supported_by_provider(self).await))
|
||||
|
||||
152
src/scheduler.rs
152
src/scheduler.rs
@@ -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,10 +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::message::MsgId;
|
||||
use crate::smtp::Smtp;
|
||||
use crate::log::LogExt;
|
||||
use crate::smtp::{send_smtp_messages, Smtp};
|
||||
|
||||
use self::connectivity::ConnectivityStore;
|
||||
|
||||
@@ -82,11 +83,15 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
let mut jobs_loaded = 0;
|
||||
let mut info = InterruptInfo::default();
|
||||
loop {
|
||||
match job::load_next(&ctx, Thread::Imap, &info)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
let job = match job::load_next(&ctx, Thread::Imap, &info).await {
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed loading job from the database: {:#}.", err);
|
||||
None
|
||||
}
|
||||
Ok(job) => job,
|
||||
};
|
||||
|
||||
match job {
|
||||
Some(job) if jobs_loaded <= 20 => {
|
||||
jobs_loaded += 1;
|
||||
job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await;
|
||||
@@ -95,41 +100,15 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
Some(job) => {
|
||||
// Let the fetch run, but return back to the job afterwards.
|
||||
jobs_loaded = 0;
|
||||
if ctx
|
||||
.get_config_bool(Config::InboxWatch)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
info!(ctx, "postponing imap-job {} to run fetch...", job);
|
||||
fetch(&ctx, &mut connection).await;
|
||||
}
|
||||
info!(ctx, "postponing imap-job {} to run fetch...", job);
|
||||
fetch(&ctx, &mut connection).await;
|
||||
}
|
||||
None => {
|
||||
jobs_loaded = 0;
|
||||
|
||||
// Expunge folder if needed, e.g. if some jobs have
|
||||
// deleted messages on the server.
|
||||
if let Err(err) = connection.maybe_close_folder(&ctx).await {
|
||||
warn!(ctx, "failed to close folder: {:?}", err);
|
||||
}
|
||||
|
||||
maybe_add_time_based_warnings(&ctx).await;
|
||||
|
||||
info = if ctx
|
||||
.get_config_bool(Config::InboxWatch)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
|
||||
} else {
|
||||
if let Err(err) = connection.scan_folders(&ctx).await {
|
||||
warn!(ctx, "{}", err);
|
||||
connection.connectivity.set_err(&ctx, err).await;
|
||||
} else {
|
||||
connection.connectivity.set_not_configured(&ctx).await;
|
||||
}
|
||||
connection.fake_idle(&ctx, None).await
|
||||
};
|
||||
info = fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,7 +136,7 @@ async fn fetch(ctx: &Context, connection: &mut Imap) {
|
||||
}
|
||||
|
||||
// fetch
|
||||
if let Err(err) = connection.fetch(ctx, &watch_folder).await {
|
||||
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
warn!(ctx, "{:#}", err);
|
||||
}
|
||||
@@ -183,22 +162,57 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
|
||||
return connection.fake_idle(ctx, Some(watch_folder)).await;
|
||||
}
|
||||
|
||||
// fetch
|
||||
if let Err(err) = connection.fetch(ctx, &watch_folder).await {
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
// Mark expired messages for deletion.
|
||||
if let Err(err) = delete_expired_imap_messages(ctx)
|
||||
.await
|
||||
.context("delete_expired_imap_messages failed")
|
||||
{
|
||||
warn!(ctx, "{:#}", err);
|
||||
return InterruptInfo::new(false, None);
|
||||
}
|
||||
|
||||
// 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
|
||||
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);
|
||||
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
|
||||
@@ -208,7 +222,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
|
||||
Err(err) => {
|
||||
connection.trigger_reconnect(ctx).await;
|
||||
warn!(ctx, "{}", err);
|
||||
InterruptInfo::new(false, None)
|
||||
InterruptInfo::new(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -293,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 {
|
||||
@@ -353,7 +375,7 @@ impl Scheduler {
|
||||
}))
|
||||
};
|
||||
|
||||
if ctx.get_config_bool(Config::MvboxWatch).await? {
|
||||
if ctx.should_watch_mvbox().await? {
|
||||
let ctx = ctx.clone();
|
||||
mvbox_handle = Some(task::spawn(async move {
|
||||
simple_imap_loop(
|
||||
@@ -437,10 +459,10 @@ impl Scheduler {
|
||||
return;
|
||||
}
|
||||
|
||||
self.interrupt_inbox(InterruptInfo::new(true, None))
|
||||
.join(self.interrupt_mvbox(InterruptInfo::new(true, None)))
|
||||
.join(self.interrupt_sentbox(InterruptInfo::new(true, None)))
|
||||
.join(self.interrupt_smtp(InterruptInfo::new(true, None)))
|
||||
self.interrupt_inbox(InterruptInfo::new(true))
|
||||
.join(self.interrupt_mvbox(InterruptInfo::new(true)))
|
||||
.join(self.interrupt_sentbox(InterruptInfo::new(true)))
|
||||
.join(self.interrupt_smtp(InterruptInfo::new(true)))
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -449,10 +471,10 @@ impl Scheduler {
|
||||
return;
|
||||
}
|
||||
|
||||
self.interrupt_inbox(InterruptInfo::new(false, None))
|
||||
.join(self.interrupt_mvbox(InterruptInfo::new(false, None)))
|
||||
.join(self.interrupt_sentbox(InterruptInfo::new(false, None)))
|
||||
.join(self.interrupt_smtp(InterruptInfo::new(false, None)))
|
||||
self.interrupt_inbox(InterruptInfo::new(false))
|
||||
.join(self.interrupt_mvbox(InterruptInfo::new(false)))
|
||||
.join(self.interrupt_sentbox(InterruptInfo::new(false)))
|
||||
.join(self.interrupt_smtp(InterruptInfo::new(false)))
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -682,14 +704,10 @@ struct ImapConnectionHandlers {
|
||||
#[derive(Default, Debug)]
|
||||
pub struct InterruptInfo {
|
||||
pub probe_network: bool,
|
||||
pub msg_id: Option<MsgId>,
|
||||
}
|
||||
|
||||
impl InterruptInfo {
|
||||
pub fn new(probe_network: bool, msg_id: Option<MsgId>) -> Self {
|
||||
Self {
|
||||
probe_network,
|
||||
msg_id,
|
||||
}
|
||||
pub fn new(probe_network: bool) -> Self {
|
||||
Self { probe_network }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -346,6 +347,10 @@ impl Context {
|
||||
<body>"#
|
||||
.to_string();
|
||||
|
||||
// =============================================================================================
|
||||
// Get the states from the RwLock
|
||||
// =============================================================================================
|
||||
|
||||
let lock = self.scheduler.read().await;
|
||||
let (folders_states, smtp) = match &*lock {
|
||||
Scheduler::Running {
|
||||
@@ -358,17 +363,14 @@ impl Context {
|
||||
[
|
||||
(
|
||||
Config::ConfiguredInboxFolder,
|
||||
Config::InboxWatch,
|
||||
inbox.state.connectivity.clone(),
|
||||
),
|
||||
(
|
||||
Config::ConfiguredMvboxFolder,
|
||||
Config::MvboxWatch,
|
||||
mvbox.state.connectivity.clone(),
|
||||
),
|
||||
(
|
||||
Config::ConfiguredSentboxFolder,
|
||||
Config::SentboxWatch,
|
||||
sentbox.state.connectivity.clone(),
|
||||
),
|
||||
],
|
||||
@@ -380,12 +382,19 @@ impl Context {
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
|
||||
for (folder, watch, state) in &folders_states {
|
||||
let w = self.get_config(*watch).await.ok_or_log(self);
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Incoming messages
|
||||
// - "Inbox": Connected
|
||||
// - "Sent": Connected
|
||||
// =============================================================================================
|
||||
|
||||
let watched_folders = get_watched_folder_configs(self).await?;
|
||||
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
|
||||
for (folder, state) in &folders_states {
|
||||
let mut folder_added = false;
|
||||
if w.flatten() == Some("1".to_string()) {
|
||||
|
||||
if watched_folders.contains(folder) {
|
||||
let f = self.get_config(*folder).await.ok_or_log(self).flatten();
|
||||
|
||||
if let Some(foldername) = f {
|
||||
@@ -417,6 +426,12 @@ impl Context {
|
||||
}
|
||||
ret += "</ul>";
|
||||
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Outgoing messages
|
||||
// Your last message was sent successfully
|
||||
// =============================================================================================
|
||||
|
||||
ret += &format!(
|
||||
"<h3>{}</h3><ul><li>",
|
||||
stock_str::outgoing_messages(self).await
|
||||
@@ -427,6 +442,13 @@ impl Context {
|
||||
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
|
||||
ret += "</li></ul>";
|
||||
|
||||
// =============================================================================================
|
||||
// Add e.g.
|
||||
// Storage on testrun.org
|
||||
// 1.34 GiB of 2 GiB used
|
||||
// [======67%===== ]
|
||||
// =============================================================================================
|
||||
|
||||
let domain = dc_tools::EmailAddress::new(
|
||||
&self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
@@ -527,6 +549,8 @@ impl Context {
|
||||
}
|
||||
ret += "</ul>";
|
||||
|
||||
// =============================================================================================
|
||||
|
||||
ret += "</body></html>\n";
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
@@ -322,10 +322,11 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
group_id,
|
||||
group_name,
|
||||
&group_id,
|
||||
&group_name,
|
||||
Blocked::Not,
|
||||
ProtectionStatus::Unprotected, // protection is added later as needed
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
@@ -333,7 +334,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
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?;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
Ok(chat_id)
|
||||
}
|
||||
}
|
||||
@@ -540,7 +541,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
bobstate.chat_id(context).await?,
|
||||
msg,
|
||||
&msg,
|
||||
time(),
|
||||
)
|
||||
.await?;
|
||||
@@ -741,7 +742,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.get_header(HeaderDef::SecureJoinGroup)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_else(|| "");
|
||||
if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid).await {
|
||||
if let Err(err) = chat::get_chat_id_by_grpid(context, field_grpid).await {
|
||||
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
|
||||
return Err(
|
||||
err.context(format!("Chat for group {} not found", &field_grpid))
|
||||
@@ -851,7 +852,7 @@ async fn secure_connection_established(
|
||||
) -> 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;
|
||||
chat::add_info_msg(context, chat_id, msg, time()).await?;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
Ok(())
|
||||
}
|
||||
@@ -873,7 +874,7 @@ async fn could_not_establish_secure_connection(
|
||||
)
|
||||
.await;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
error!(
|
||||
warn!(
|
||||
context,
|
||||
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
|
||||
);
|
||||
@@ -936,36 +937,29 @@ fn encrypted_and_signed(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use async_std::prelude::*;
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::events::Event;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::test_utils::TestContext;
|
||||
use std::time::Duration;
|
||||
use crate::test_utils::{LogSink, TestContext};
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
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);
|
||||
|
||||
// Setup JoinerProgress sinks.
|
||||
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
|
||||
bob.add_event_sink(move |event: Event| {
|
||||
let joiner_progress_tx = joiner_progress_tx.clone();
|
||||
async move {
|
||||
if let EventType::SecurejoinJoinerProgress { .. } = event.typ {
|
||||
joiner_progress_tx.try_send(event).unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = dc_get_securejoin_qr(&alice.ctx, None).await?;
|
||||
|
||||
@@ -975,7 +969,7 @@ mod tests {
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!bob.ctx.has_ongoing().await);
|
||||
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
|
||||
assert_eq!(sent.recipient(), "alice@example.org".parse().unwrap());
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
|
||||
@@ -997,28 +991,24 @@ mod tests {
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
{
|
||||
let evt = joiner_progress_rx
|
||||
.recv()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.await
|
||||
.expect("timeout waiting for JoinerProgress event")
|
||||
.expect("missing JoinerProgress event");
|
||||
match evt.typ {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => panic!("Wrong event type"),
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Check Bob sent the right message.
|
||||
@@ -1095,7 +1085,7 @@ mod tests {
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
@@ -1130,7 +1120,7 @@ mod tests {
|
||||
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
|
||||
assert!(msg.is_info());
|
||||
let text = msg.get_text().unwrap();
|
||||
assert!(text.contains("alice@example.com verified"));
|
||||
assert!(text.contains("alice@example.org verified"));
|
||||
}
|
||||
|
||||
// Check Bob sent the final message
|
||||
@@ -1153,25 +1143,22 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Setup JoinerProgress sinks.
|
||||
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
|
||||
bob.add_event_sink(move |event: Event| {
|
||||
let joiner_progress_tx = joiner_progress_tx.clone();
|
||||
async move {
|
||||
if let EventType::SecurejoinJoinerProgress { .. } = event.typ {
|
||||
joiner_progress_tx.try_send(event).unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
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;
|
||||
|
||||
// Ensure Bob knows Alice_FP
|
||||
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await?;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.com".into(),
|
||||
addr: "alice@example.org".into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 10,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
@@ -1194,30 +1181,25 @@ mod tests {
|
||||
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
{
|
||||
let evt = joiner_progress_rx
|
||||
.recv()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.await
|
||||
.expect("timeout waiting for JoinerProgress event")
|
||||
.expect("missing JoinerProgress event");
|
||||
match evt.typ {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => panic!("Wrong event type"),
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert!(!bob.ctx.has_ongoing().await);
|
||||
|
||||
// Check Bob sent the right handshake message.
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
@@ -1265,7 +1247,7 @@ mod tests {
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
@@ -1294,8 +1276,17 @@ mod tests {
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact_concurrent_calls() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
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;
|
||||
|
||||
// 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";
|
||||
@@ -1317,30 +1308,27 @@ mod tests {
|
||||
.pop_sent_msg()
|
||||
.await
|
||||
.payload()
|
||||
.contains("alice@example.com"));
|
||||
.contains("alice@example.org"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_secure_join() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
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);
|
||||
|
||||
// Setup JoinerProgress sinks.
|
||||
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
|
||||
bob.add_event_sink(move |event: Event| {
|
||||
let joiner_progress_tx = joiner_progress_tx.clone();
|
||||
async move {
|
||||
if let EventType::SecurejoinJoinerProgress { .. } = event.typ {
|
||||
joiner_progress_tx.try_send(event).unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let chatid =
|
||||
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
|
||||
|
||||
@@ -1354,7 +1342,7 @@ mod tests {
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
|
||||
assert_eq!(sent.recipient(), "alice@example.org".parse().unwrap());
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
|
||||
@@ -1376,28 +1364,24 @@ mod tests {
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
{
|
||||
let evt = joiner_progress_rx
|
||||
.recv()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.await
|
||||
.expect("timeout waiting for JoinerProgress event")
|
||||
.expect("missing JoinerProgress event");
|
||||
match evt.typ {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => panic!("Wrong event type"),
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Check Bob sent the right handshake message.
|
||||
@@ -1442,7 +1426,7 @@ mod tests {
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown)
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
|
||||
@@ -71,7 +71,7 @@ impl<'a> BobStateHandle<'a> {
|
||||
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? {
|
||||
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, grpid).await? {
|
||||
Ok(chat_id)
|
||||
} else {
|
||||
bail!("chat not found")
|
||||
@@ -422,7 +422,7 @@ impl BobState {
|
||||
BobHandshakeMsg::Request => {
|
||||
// Sends the Secure-Join-Invitenumber header in mimefactory.rs.
|
||||
msg.param.set(Param::Arg2, self.invite.invitenumber());
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.force_plaintext();
|
||||
}
|
||||
BobHandshakeMsg::RequestWithAuth => {
|
||||
// Sends the Secure-Join-Auth header in mimefactory.rs.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! # Simplify incoming plaintext.
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
// protect lines starting with `--` against being treated as a footer.
|
||||
// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
|
||||
// this should be invisible on most systems and there is no need to unescape it again
|
||||
@@ -158,6 +156,7 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
|
||||
s.strip_prefix('>')
|
||||
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
|
||||
l_last -= 1
|
||||
@@ -204,6 +203,7 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
|
||||
s.strip_prefix('>')
|
||||
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n"),
|
||||
),
|
||||
)
|
||||
|
||||
331
src/smtp.rs
331
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,289 @@ 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.
|
||||
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, "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 {
|
||||
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(()) => 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);
|
||||
}
|
||||
|
||||
let (body, recipients, msg_id) = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT mime, recipients, msg_id 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)?;
|
||||
Ok((mime, recipients, msg_id))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
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) => {
|
||||
if res.is_ok() {
|
||||
msg_id.set_delivered(context).await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
|
||||
.await?;
|
||||
}
|
||||
res
|
||||
}
|
||||
Status::RetryNow | Status::RetryLater => {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE smtp SET retries=retries+1 WHERE id=?",
|
||||
paramsv![rowid],
|
||||
)
|
||||
.await
|
||||
.context("failed to update retries count")?;
|
||||
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.
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE retries > 5", paramsv![])
|
||||
.await?;
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use async_smtp::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
use crate::constants::DEFAULT_MAX_SMTP_RCPT_TO;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use itertools::Itertools;
|
||||
use std::time::Duration;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -29,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();
|
||||
|
||||
@@ -42,15 +41,19 @@ impl Smtp {
|
||||
}
|
||||
}
|
||||
|
||||
for recipients_chunk in recipients.chunks(chunk_size).into_iter() {
|
||||
let recipients = recipients_chunk.to_vec();
|
||||
let recipients_display = recipients.iter().map(|x| x.to_string()).join(",");
|
||||
for recipients_chunk in recipients.chunks(chunk_size) {
|
||||
let recipients_display = recipients_chunk
|
||||
.iter()
|
||||
.map(|x| x.as_ref())
|
||||
.collect::<Vec<&str>>()
|
||||
.join(",");
|
||||
|
||||
let envelope = Envelope::new(self.from.clone(), recipients).map_err(Error::Envelope)?;
|
||||
let envelope = Envelope::new(self.from.clone(), recipients_chunk.to_vec())
|
||||
.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 {
|
||||
|
||||
474
src/sql.rs
474
src/sql.rs
@@ -7,9 +7,10 @@ use std::collections::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};
|
||||
@@ -38,20 +39,50 @@ 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>>,
|
||||
}
|
||||
|
||||
impl Sql {
|
||||
pub fn new() -> Sql {
|
||||
Self::default()
|
||||
pub fn new(dbfile: PathBuf) -> Sql {
|
||||
Self {
|
||||
dbfile,
|
||||
pool: Default::default(),
|
||||
is_encrypted: 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 +90,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,105 +194,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?;
|
||||
// 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.
|
||||
@@ -219,10 +331,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.
|
||||
@@ -251,9 +363,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)
|
||||
@@ -604,6 +714,16 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
|
||||
context.schedule_quota_update().await?;
|
||||
|
||||
// Try to clear the freelist to free some space on the disk. This
|
||||
// only works if auto_vacuum is enabled.
|
||||
if let Err(err) = context
|
||||
.sql
|
||||
.execute("PRAGMA incremental_vacuum", paramsv![])
|
||||
.await
|
||||
{
|
||||
warn!(context, "Failed to run incremental vacuum: {}", err);
|
||||
}
|
||||
|
||||
if let Err(e) = context
|
||||
.set_config(Config::LastHousekeeping, Some(&time().to_string()))
|
||||
.await
|
||||
@@ -666,9 +786,11 @@ async fn maybe_add_from_param(
|
||||
/// have a server UID.
|
||||
async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
sql.execute(
|
||||
"DELETE FROM msgs \
|
||||
WHERE (chat_id = ? OR hidden) \
|
||||
AND server_uid = 0",
|
||||
"DELETE FROM msgs
|
||||
WHERE (chat_id=? OR hidden)
|
||||
AND NOT EXISTS (
|
||||
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
|
||||
)",
|
||||
paramsv![DC_CHAT_ID_TRASH],
|
||||
)
|
||||
.await?;
|
||||
@@ -676,11 +798,12 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
mod tests {
|
||||
use async_std::channel;
|
||||
use async_std::fs::File;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::{test_utils::TestContext, Event, EventType};
|
||||
use crate::{test_utils::TestContext, EventType};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -725,6 +848,22 @@ mod test {
|
||||
assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap());
|
||||
}
|
||||
|
||||
/// Tests that auto_vacuum is enabled for new databases.
|
||||
#[async_std::test]
|
||||
async fn test_auto_vacuum() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let conn = t.sql.get_conn().await?;
|
||||
let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| {
|
||||
let auto_vacuum: i32 = row.get(0)?;
|
||||
Ok(auto_vacuum)
|
||||
})?;
|
||||
|
||||
// auto_vacuum=2 is the same as auto_vacuum=INCREMENTAL
|
||||
assert_eq!(auto_vacuum, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_housekeeping_db_closed() {
|
||||
let t = TestContext::new().await;
|
||||
@@ -741,7 +880,20 @@ mod test {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
t.add_event_sink(move |event: Event| async move {
|
||||
let (event_sink, event_source) = channel::unbounded();
|
||||
t.add_event_sender(event_sink).await;
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
|
||||
|
||||
t.sql.close().await;
|
||||
housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed
|
||||
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()[..]);
|
||||
|
||||
while let Ok(event) = event_source.try_recv() {
|
||||
match event.typ {
|
||||
EventType::Info(s) => assert!(
|
||||
!s.contains("Keeping new unreferenced file"),
|
||||
@@ -751,18 +903,7 @@ mod test {
|
||||
EventType::Error(s) => panic!("{}", s),
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
|
||||
|
||||
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();
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test.
|
||||
@@ -787,14 +928,14 @@ mod test {
|
||||
// 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"),
|
||||
@@ -824,20 +965,59 @@ mod test {
|
||||
assert!(!disable_server_delete);
|
||||
assert!(!recode_avatar);
|
||||
|
||||
info!(&t, "test_migration_flags: XXX");
|
||||
info!(&t, "test_migration_flags: XXX END MARKER");
|
||||
|
||||
loop {
|
||||
if let EventType::Info(info) = t.evtracker.recv().await.unwrap() {
|
||||
assert!(
|
||||
!info.contains("[migration]"),
|
||||
"Migrations were run twice, you probably forgot to update the db version"
|
||||
);
|
||||
if info.contains("test_migration_flags: XXX") {
|
||||
break;
|
||||
let evt = t
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::Info(_)))
|
||||
.await;
|
||||
match evt {
|
||||
EventType::Info(msg) => {
|
||||
assert!(
|
||||
!msg.contains("[migration]"),
|
||||
"Migrations were run twice, you probably forgot to update the db version"
|
||||
);
|
||||
if msg.contains("test_migration_flags: XXX END MARKER") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
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)?;
|
||||
@@ -497,6 +501,84 @@ item TEXT DEFAULT '');"#,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 81 {
|
||||
info!(context, "[migration] v81");
|
||||
sql.execute_migration("ALTER TABLE msgs ADD COLUMN hop_info TEXT;", 81)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 82 {
|
||||
info!(context, "[migration] v82");
|
||||
sql.execute_migration(
|
||||
r#"CREATE TABLE imap (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc724_mid TEXT DEFAULT '', -- Message-ID header
|
||||
folder TEXT DEFAULT '', -- IMAP folder
|
||||
target TEXT DEFAULT '', -- Destination folder, empty to delete.
|
||||
uid INTEGER DEFAULT 0, -- UID
|
||||
uidvalidity INTEGER DEFAULT 0,
|
||||
UNIQUE (folder, uid, uidvalidity)
|
||||
);
|
||||
CREATE INDEX imap_folder ON imap(folder);
|
||||
CREATE INDEX imap_messageid ON imap(rfc724_mid);
|
||||
|
||||
INSERT INTO imap
|
||||
(rfc724_mid, folder, target, uid, uidvalidity)
|
||||
SELECT
|
||||
rfc724_mid,
|
||||
server_folder AS folder,
|
||||
server_folder AS target,
|
||||
server_uid AS uid,
|
||||
(SELECT uidvalidity FROM imap_sync WHERE folder=server_folder) AS uidvalidity
|
||||
FROM msgs
|
||||
WHERE server_uid>0
|
||||
ON CONFLICT (folder, uid, uidvalidity)
|
||||
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
|
||||
target=excluded.target;
|
||||
"#,
|
||||
82,
|
||||
)
|
||||
.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?;
|
||||
}
|
||||
|
||||
Ok((
|
||||
recalc_fingerprints,
|
||||
@@ -524,7 +606,8 @@ impl Sql {
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
.await
|
||||
.with_context(|| format!("execute_migration failed for version {}", version))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use strum::EnumProperty;
|
||||
use strum_macros::EnumProperty;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, ChatId, ProtectionStatus};
|
||||
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::contact::{Contact, Origin};
|
||||
@@ -53,12 +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 = "Hello, I\'ve just created the group \"%1$s\" for us."))]
|
||||
NewGroupDraft = 14,
|
||||
|
||||
#[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\"."))]
|
||||
MsgGrpName = 15,
|
||||
|
||||
@@ -333,6 +327,12 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
|
||||
SecureJoinReplies = 118,
|
||||
|
||||
#[strum(props(fallback = "Scan to chat with %1$s"))]
|
||||
SetupContactQRDescription = 119,
|
||||
|
||||
#[strum(props(fallback = "Scan to join group %1$s"))]
|
||||
SecureJoinGroupQRDescription = 120,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -452,18 +452,6 @@ 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: `Hello, I've just created the group "%1$s" for us.`.
|
||||
pub(crate) async fn new_group_draft(context: &Context, group_name: impl AsRef<str>) -> String {
|
||||
translated(context, StockMessage::NewGroupDraft)
|
||||
.await
|
||||
.replace1(group_name)
|
||||
}
|
||||
|
||||
/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
|
||||
pub(crate) async fn msg_grp_name(
|
||||
context: &Context,
|
||||
@@ -624,6 +612,29 @@ pub(crate) async fn secure_join_replies(context: &Context, contact_id: u32) -> S
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to chat with %1$s`.
|
||||
pub(crate) async fn setup_contact_qr_description(
|
||||
context: &Context,
|
||||
display_name: &str,
|
||||
addr: &str,
|
||||
) -> String {
|
||||
let name = if display_name == addr {
|
||||
addr.to_owned()
|
||||
} else {
|
||||
format!("{} ({})", display_name, addr)
|
||||
};
|
||||
translated(context, StockMessage::SetupContactQRDescription)
|
||||
.await
|
||||
.replace1(name)
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to join %1$s`.
|
||||
pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
|
||||
translated(context, StockMessage::SecureJoinGroupQRDescription)
|
||||
.await
|
||||
.replace1(chat.get_name())
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s verified.`.
|
||||
pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef<str>) -> String {
|
||||
translated(context, StockMessage::ContactVerified)
|
||||
@@ -1210,20 +1221,20 @@ mod tests {
|
||||
async fn test_stock_system_msg_add_member_by_me() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
msg_add_member(&t, "alice@example.com", DC_CONTACT_ID_SELF).await,
|
||||
"Member alice@example.com added by me."
|
||||
msg_add_member(&t, "alice@example.org", DC_CONTACT_ID_SELF).await,
|
||||
"Member alice@example.org added by me."
|
||||
)
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
Contact::create(&t, "Alice", "alice@example.com")
|
||||
Contact::create(&t, "Alice", "alice@example.org")
|
||||
.await
|
||||
.expect("failed to create contact");
|
||||
assert_eq!(
|
||||
msg_add_member(&t, "alice@example.com", DC_CONTACT_ID_SELF).await,
|
||||
"Member Alice (alice@example.com) added by me."
|
||||
msg_add_member(&t, "alice@example.org", DC_CONTACT_ID_SELF).await,
|
||||
"Member Alice (alice@example.org) added by me."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1231,7 +1242,7 @@ mod tests {
|
||||
async fn test_stock_system_msg_add_member_by_other_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = {
|
||||
Contact::create(&t, "Alice", "alice@example.com")
|
||||
Contact::create(&t, "Alice", "alice@example.org")
|
||||
.await
|
||||
.expect("Failed to create contact Alice");
|
||||
Contact::create(&t, "Bob", "bob@example.com")
|
||||
@@ -1239,8 +1250,8 @@ mod tests {
|
||||
.expect("failed to create bob")
|
||||
};
|
||||
assert_eq!(
|
||||
msg_add_member(&t, "alice@example.com", contact_id,).await,
|
||||
"Member Alice (alice@example.com) added by Bob (bob@example.com)."
|
||||
msg_add_member(&t, "alice@example.org", contact_id,).await,
|
||||
"Member Alice (alice@example.org) added by Bob (bob@example.com)."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1269,11 +1280,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
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::message::{Message, MessageState};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::stock_str;
|
||||
use itertools::Itertools;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
@@ -138,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 {
|
||||
@@ -174,7 +180,7 @@ impl Message {
|
||||
summary_content
|
||||
};
|
||||
|
||||
summary.split_whitespace().join(" ")
|
||||
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
|
||||
use crate::token::Namespace;
|
||||
use crate::{chat, stock_str, token};
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use lettre_email::mime::{self};
|
||||
use lettre_email::PartBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -177,7 +176,10 @@ impl Context {
|
||||
} else {
|
||||
Ok(Some((
|
||||
format!("{{\"items\":[\n{}\n]}}", serialized),
|
||||
ids.iter().map(|x| x.to_string()).join(","),
|
||||
ids.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(","),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
//!
|
||||
//! This private module is only compiled for test runs.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::panic;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{collections::BTreeMap, panic};
|
||||
use std::{fmt, thread};
|
||||
|
||||
use ansi_term::Color;
|
||||
use async_std::channel::Receiver;
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::channel::{self, Receiver, Sender};
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::{Arc, RwLock};
|
||||
use async_std::{channel, pin::Pin};
|
||||
use async_std::{future::Future, task};
|
||||
use async_std::task;
|
||||
use chat::ChatItem;
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
@@ -28,50 +28,107 @@ 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};
|
||||
use crate::key::{self, DcKey, KeyPair, KeyPairUse};
|
||||
use crate::message::{update_msg_state, Message, MessageState, MsgId};
|
||||
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");
|
||||
|
||||
type EventSink =
|
||||
dyn Fn(Event) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> + Send + Sync + 'static;
|
||||
|
||||
/// Map of [`Context::id`] to names for [`TestContext`]s.
|
||||
static CONTEXT_NAMES: Lazy<std::sync::RwLock<BTreeMap<u32, String>>> =
|
||||
Lazy::new(|| std::sync::RwLock::new(BTreeMap::new()));
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TestContextBuilder {
|
||||
key_pair: Option<KeyPair>,
|
||||
log_sink: Option<Sender<Event>>,
|
||||
}
|
||||
|
||||
impl TestContextBuilder {
|
||||
/// Configures as alice@example.org with fixed secret key.
|
||||
///
|
||||
/// This is a shortcut for `.with_key_pair(alice_keypair()).
|
||||
pub fn configure_alice(self) -> Self {
|
||||
self.with_key_pair(alice_keypair())
|
||||
}
|
||||
|
||||
/// Configures as bob@example.net with fixed secret key.
|
||||
///
|
||||
/// This is a shortcut for `.with_key_pair(bob_keypair()).
|
||||
pub fn configure_bob(self) -> Self {
|
||||
self.with_key_pair(bob_keypair())
|
||||
}
|
||||
|
||||
/// Configures the new [`TestContext`] with the provided [`KeyPair`].
|
||||
///
|
||||
/// This will extract the email address from the key and configure the context with the
|
||||
/// given identity.
|
||||
pub fn with_key_pair(mut self, key_pair: KeyPair) -> Self {
|
||||
self.key_pair = Some(key_pair);
|
||||
self
|
||||
}
|
||||
|
||||
/// Attaches a [`LogSink`] to this [`TestContext`].
|
||||
///
|
||||
/// This is useful when using multiple [`TestContext`] instances in one test: it allows
|
||||
/// using a single [`LogSink`] for both contexts. This shows the log messages in
|
||||
/// sequence as they occurred rather than all messages from each context in a single
|
||||
/// block.
|
||||
pub fn with_log_sink(mut self, sink: Sender<Event>) -> Self {
|
||||
self.log_sink = Some(sink);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the [`TestContext`].
|
||||
pub async fn build(self) -> TestContext {
|
||||
let name = self.key_pair.as_ref().map(|key| key.addr.local.clone());
|
||||
|
||||
let test_context = TestContext::new_internal(name, self.log_sink).await;
|
||||
|
||||
if let Some(key_pair) = self.key_pair {
|
||||
test_context
|
||||
.configure_addr(&key_pair.addr.to_string())
|
||||
.await;
|
||||
key::store_self_keypair(&test_context, &key_pair, KeyPairUse::Default)
|
||||
.await
|
||||
.expect("Failed to save key");
|
||||
}
|
||||
test_context
|
||||
}
|
||||
}
|
||||
|
||||
/// A Context and temporary directory.
|
||||
///
|
||||
/// The temporary directory can be used to store the SQLite database,
|
||||
/// see e.g. [test_context] which does this.
|
||||
pub(crate) struct TestContext {
|
||||
#[derive(Debug)]
|
||||
pub struct TestContext {
|
||||
pub ctx: Context,
|
||||
pub dir: TempDir,
|
||||
pub evtracker: EvTracker,
|
||||
/// Counter for fake IMAP UIDs in [recv_msg], for private use in that function only.
|
||||
recv_idx: RwLock<u32>,
|
||||
/// Functions to call for events received.
|
||||
event_sinks: Arc<RwLock<Vec<Box<EventSink>>>>,
|
||||
pub evtracker: EventTracker,
|
||||
/// Channels which should receive events from this context.
|
||||
event_senders: Arc<RwLock<Vec<Sender<Event>>>>,
|
||||
/// Receives panics from sinks ("sink" means "event handler" here)
|
||||
poison_receiver: channel::Receiver<String>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for TestContext {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TestContext")
|
||||
.field("ctx", &self.ctx)
|
||||
.field("dir", &self.dir)
|
||||
.field("recv_idx", &self.recv_idx)
|
||||
.field("event_sinks", &String::from("Vec<EventSink>"))
|
||||
.finish()
|
||||
}
|
||||
poison_receiver: Receiver<String>,
|
||||
/// Reference to implicit [`LogSink`] so it is dropped together with the context.
|
||||
///
|
||||
/// Only used if no explicit `log_sender` is passed into [`TestContext::new_internal`]
|
||||
/// (which is assumed to be the sending end of a [`LogSink`]).
|
||||
///
|
||||
/// This is a convenience in case only a single [`TestContext`] is used to avoid dealing
|
||||
/// with [`LogSink`]. Never read, thus "dead code", since the only purpose is to
|
||||
/// control when Drop is invoked.
|
||||
#[allow(dead_code)]
|
||||
log_sink: Option<LogSink>,
|
||||
}
|
||||
|
||||
impl TestContext {
|
||||
/// Returns the builder to have more control over creating the context.
|
||||
pub fn builder() -> TestContextBuilder {
|
||||
TestContextBuilder::default()
|
||||
}
|
||||
|
||||
/// Creates a new [`TestContext`].
|
||||
///
|
||||
/// The [Context] will be created and have an SQLite database named "db.sqlite" in the
|
||||
@@ -80,18 +137,33 @@ impl TestContext {
|
||||
///
|
||||
/// [Context]: crate::context::Context
|
||||
pub async fn new() -> Self {
|
||||
Self::new_named(None).await
|
||||
Self::new_internal(None, None).await
|
||||
}
|
||||
|
||||
/// Creates a new [`TestContext`] with a set name used in event logging.
|
||||
pub async fn with_name(name: impl Into<String>) -> Self {
|
||||
Self::new_named(Some(name.into())).await
|
||||
/// Creates a new configured [`TestContext`].
|
||||
///
|
||||
/// This is a shortcut which automatically calls [`TestContext::configure_alice`] after
|
||||
/// creating the context.
|
||||
pub async fn new_alice() -> Self {
|
||||
Self::builder().configure_alice().build().await
|
||||
}
|
||||
|
||||
async fn new_named(name: Option<String>) -> Self {
|
||||
use rand::Rng;
|
||||
pretty_env_logger::try_init().ok();
|
||||
/// Creates a new configured [`TestContext`].
|
||||
///
|
||||
/// This is a shortcut which configures bob@example.net with a fixed key.
|
||||
pub async fn new_bob() -> Self {
|
||||
Self::builder().configure_bob().build().await
|
||||
}
|
||||
|
||||
/// Internal constructor.
|
||||
///
|
||||
/// `name` is used to identify this context in e.g. log output. This is useful mostly
|
||||
/// when you have multiple [`TestContext`]s in a test.
|
||||
///
|
||||
/// `log_sender` is assumed to be the sender for a [`LogSink`]. If not supplied a new
|
||||
/// [`LogSink`] will be created so that events are logged to this test when the
|
||||
/// [`TestContext`] is dropped.
|
||||
async fn new_internal(name: Option<String>, log_sender: Option<Sender<Event>>) -> Self {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = rand::thread_rng().gen();
|
||||
@@ -99,18 +171,26 @@ impl TestContext {
|
||||
let mut context_names = CONTEXT_NAMES.write().unwrap();
|
||||
context_names.insert(id, name);
|
||||
}
|
||||
let ctx = Context::new("FakeOS".into(), dbfile.into(), id)
|
||||
let ctx = Context::new(dbfile.into(), id)
|
||||
.await
|
||||
.expect("failed to create context");
|
||||
|
||||
let events = ctx.get_event_emitter();
|
||||
|
||||
let event_sinks: Arc<RwLock<Vec<Box<EventSink>>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
let sinks = Arc::clone(&event_sinks);
|
||||
let (poison_sender, poison_receiver) = channel::bounded(1);
|
||||
let (evtracker_sender, evtracker_receiver) = channel::unbounded();
|
||||
let (log_sender, log_sink) = match log_sender {
|
||||
Some(sender) => (sender, None),
|
||||
None => {
|
||||
let (sender, sink) = LogSink::create();
|
||||
(sender, Some(sink))
|
||||
}
|
||||
};
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
let (evtracker_sender, evtracker_receiver) = channel::unbounded();
|
||||
let event_senders = Arc::new(RwLock::new(vec![log_sender, evtracker_sender]));
|
||||
let senders = Arc::clone(&event_senders);
|
||||
let (poison_sender, poison_receiver) = channel::bounded(1);
|
||||
|
||||
task::spawn(async move {
|
||||
// Make sure that the test fails if there is a panic on this thread here
|
||||
// (but not if there is a panic on another thread)
|
||||
let looptask_id = task::current().id();
|
||||
@@ -126,50 +206,26 @@ impl TestContext {
|
||||
|
||||
while let Some(event) = events.recv().await {
|
||||
{
|
||||
log::debug!("{:?}", event);
|
||||
let sinks = sinks.read().await;
|
||||
for sink in sinks.iter() {
|
||||
sink(event.clone()).await;
|
||||
let sinks = senders.read().await;
|
||||
for sender in sinks.iter() {
|
||||
// Don't block because someone wanted to use a oneshot receiver, use
|
||||
// an unbounded channel if you want all events.
|
||||
sender.try_send(event.clone()).ok();
|
||||
}
|
||||
}
|
||||
receive_event(&event);
|
||||
evtracker_sender.send(event.typ).await.ok();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
ctx,
|
||||
dir,
|
||||
evtracker: EvTracker(evtracker_receiver),
|
||||
recv_idx: RwLock::new(0),
|
||||
event_sinks,
|
||||
evtracker: EventTracker(evtracker_receiver),
|
||||
event_senders,
|
||||
poison_receiver,
|
||||
log_sink,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new configured [`TestContext`].
|
||||
///
|
||||
/// This is a shortcut which automatically calls [`TestContext::configure_alice`] after
|
||||
/// creating the context.
|
||||
pub async fn new_alice() -> Self {
|
||||
let t = Self::with_name("alice").await;
|
||||
t.configure_alice().await;
|
||||
t
|
||||
}
|
||||
|
||||
/// Creates a new configured [`TestContext`].
|
||||
///
|
||||
/// This is a shortcut which configures bob@example.net with a fixed key.
|
||||
pub async fn new_bob() -> Self {
|
||||
let t = Self::with_name("bob").await;
|
||||
let keypair = bob_keypair();
|
||||
t.configure_addr(&keypair.addr.to_string()).await;
|
||||
key::store_self_keypair(&t, &keypair, key::KeyPairUse::Default)
|
||||
.await
|
||||
.expect("Failed to save Bob's key");
|
||||
t
|
||||
}
|
||||
|
||||
/// Sets a name for this [`TestContext`] if one isn't yet set.
|
||||
///
|
||||
/// This will show up in events logged in the test output.
|
||||
@@ -180,32 +236,12 @@ impl TestContext {
|
||||
.or_insert_with(|| name.into());
|
||||
}
|
||||
|
||||
/// Add a new callback which will receive events.
|
||||
/// Adds a new [`Event`]s sender.
|
||||
///
|
||||
/// The test context runs an async task receiving all events from the [`Context`], which
|
||||
/// are logged to stdout. This allows you to register additional callbacks which will
|
||||
/// receive all events in case your tests need to watch for a specific event.
|
||||
pub async fn add_event_sink<F, R>(&self, sink: F)
|
||||
where
|
||||
// Aka `F: EventSink` but type aliases are not allowed.
|
||||
F: Fn(Event) -> R + Send + Sync + 'static,
|
||||
R: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let mut sinks = self.event_sinks.write().await;
|
||||
sinks.push(Box::new(move |evt| Box::pin(sink(evt))));
|
||||
}
|
||||
|
||||
/// Configure with alice@example.com.
|
||||
///
|
||||
/// The context will be fake-configured as the alice user, with a pre-generated secret
|
||||
/// key. The email address of the user is returned as a string.
|
||||
pub async fn configure_alice(&self) -> String {
|
||||
let keypair = alice_keypair();
|
||||
self.configure_addr(&keypair.addr.to_string()).await;
|
||||
key::store_self_keypair(&self.ctx, &keypair, key::KeyPairUse::Default)
|
||||
.await
|
||||
.expect("Failed to save Alice's key");
|
||||
keypair.addr.to_string()
|
||||
/// Once added, all events emitted by this context will be sent to this channel. This
|
||||
/// is useful if you need to wait for events or make assertions on them.
|
||||
pub async fn add_event_sender(&self, sink: Sender<Event>) {
|
||||
self.event_senders.write().await.push(sink)
|
||||
}
|
||||
|
||||
/// Configure as a given email address.
|
||||
@@ -235,27 +271,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) {
|
||||
@@ -264,24 +300,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,13 +332,11 @@ impl TestContext {
|
||||
///
|
||||
/// Receives a message using the `dc_receive_imf()` pipeline.
|
||||
pub async fn recv_msg(&self, msg: &SentMessage) {
|
||||
let mut idx = self.recv_idx.write().await;
|
||||
*idx += 1;
|
||||
let received_msg =
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n"
|
||||
.to_owned()
|
||||
+ &msg.payload();
|
||||
dc_receive_imf(&self.ctx, received_msg.as_bytes(), "INBOX", *idx, false)
|
||||
+ msg.payload();
|
||||
dc_receive_imf(&self.ctx, received_msg.as_bytes(), "INBOX", false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -514,14 +542,46 @@ impl Drop for TestContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// A receiver of [`Event`]s which will log the events to the captured test stdout.
|
||||
///
|
||||
/// Tests redirect the stdout of the test thread and capture this, showing the captured
|
||||
/// stdout if the test fails. This means printing log messages must be done on the thread
|
||||
/// of the test itself and not from a spawned task.
|
||||
///
|
||||
/// This sink achieves this by printing the events, in the order received, at the time it is
|
||||
/// dropped. Thus to use you must only make sure this sink is dropped in the test itself.
|
||||
///
|
||||
/// To use this create an instance using [`LogSink::create`] and then use the
|
||||
/// [`TestContextBuilder::with_log_sink`].
|
||||
#[derive(Debug)]
|
||||
pub struct LogSink {
|
||||
events: Receiver<Event>,
|
||||
}
|
||||
|
||||
impl LogSink {
|
||||
/// Creates a new [`LogSink`] and returns the attached event sink.
|
||||
pub fn create() -> (Sender<Event>, Self) {
|
||||
let (tx, rx) = channel::unbounded();
|
||||
(tx, Self { events: rx })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LogSink {
|
||||
fn drop(&mut self) {
|
||||
while let Ok(event) = self.events.try_recv() {
|
||||
print_event(&event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A raw message as it was scheduled to be sent.
|
||||
///
|
||||
/// This is a raw message, probably in the shape DC was planning to send it but not having
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -530,33 +590,34 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a pre-generated keypair for alice@example.com from disk.
|
||||
/// Load a pre-generated keypair for alice@example.org from disk.
|
||||
///
|
||||
/// This saves CPU cycles by avoiding having to generate a key.
|
||||
///
|
||||
/// The keypair was created using the crate::key::tests::gen_key test.
|
||||
pub fn alice_keypair() -> key::KeyPair {
|
||||
let addr = EmailAddress::new("alice@example.com").unwrap();
|
||||
let public =
|
||||
key::SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
|
||||
.unwrap();
|
||||
let secret =
|
||||
key::SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
|
||||
.unwrap();
|
||||
pub fn alice_keypair() -> KeyPair {
|
||||
let addr = EmailAddress::new("alice@example.org").unwrap();
|
||||
|
||||
let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/alice-public.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
@@ -567,12 +628,14 @@ pub fn alice_keypair() -> key::KeyPair {
|
||||
/// Load a pre-generated keypair for bob@example.net from disk.
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub fn bob_keypair() -> key::KeyPair {
|
||||
pub fn bob_keypair() -> KeyPair {
|
||||
let addr = EmailAddress::new("bob@example.net").unwrap();
|
||||
let public =
|
||||
key::SignedPublicKey::from_base64(include_str!("../test-data/key/bob-public.asc")).unwrap();
|
||||
let secret =
|
||||
key::SignedSecretKey::from_base64(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
||||
let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/bob-public.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
@@ -580,40 +643,43 @@ pub fn bob_keypair() -> key::KeyPair {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EvTracker(Receiver<EventType>);
|
||||
/// Utility to help wait for and retrieve events.
|
||||
///
|
||||
/// This buffers the events in order they are emitted. This allows consuming events in
|
||||
/// order while looking for the right events using the provided methods.
|
||||
///
|
||||
/// The methods only return [`EventType`] rather than the full [`Event`] since it can only
|
||||
/// be attached to a single [`TestContext`] and therefore the context is already known as
|
||||
/// you will be accessing it as [`TestContext::evtracker`].
|
||||
#[derive(Debug)]
|
||||
pub struct EventTracker(Receiver<Event>);
|
||||
|
||||
impl EvTracker {
|
||||
pub async fn get_info_contains(&self, s: &str) -> EventType {
|
||||
loop {
|
||||
let event = self.0.recv().await.unwrap();
|
||||
if let EventType::Info(i) = &event {
|
||||
if i.contains(s) {
|
||||
return event;
|
||||
impl EventTracker {
|
||||
/// Consumes emitted events returning the first matching one.
|
||||
///
|
||||
/// If no matching events are ready this will wait for new events to arrive and time out
|
||||
/// after 10 seconds.
|
||||
pub async fn get_matching<F: Fn(&EventType) -> bool>(&self, event_matcher: F) -> EventType {
|
||||
async move {
|
||||
loop {
|
||||
let event = self.0.recv().await.unwrap();
|
||||
if event_matcher(&event.typ) {
|
||||
return event.typ;
|
||||
}
|
||||
}
|
||||
}
|
||||
.timeout(Duration::from_secs(10))
|
||||
.await
|
||||
.expect("timeout waiting for event match")
|
||||
}
|
||||
|
||||
pub async fn get_matching<F: Fn(EventType) -> bool>(&self, event_matcher: F) -> EventType {
|
||||
const TIMEOUT: Duration = Duration::from_secs(20);
|
||||
|
||||
loop {
|
||||
let event = async_std::future::timeout(TIMEOUT, self.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
if event_matcher(event.clone()) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EvTracker {
|
||||
type Target = Receiver<EventType>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
/// Consumes events looking for an [`EventType::Info`] with substring matching.
|
||||
pub async fn get_info_contains(&self, s: &str) -> EventType {
|
||||
self.get_matching(|evt| match evt {
|
||||
EventType::Info(ref msg) => msg.contains(s),
|
||||
_ => false,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,7 +706,7 @@ pub(crate) async fn get_chat_msg(
|
||||
/// Pretty-print an event to stdout
|
||||
///
|
||||
/// Done during tests this is captured by `cargo test` and associated with the test itself.
|
||||
fn receive_event(event: &Event) {
|
||||
fn print_event(event: &Event) {
|
||||
let green = Color::Green.normal();
|
||||
let yellow = Color::Yellow.normal();
|
||||
let red = Color::Red.normal();
|
||||
@@ -759,3 +825,43 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
statestr,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// The following three tests demonstrate, when made to fail, the log output being
|
||||
// directed to the correct test output.
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_with_alice() {
|
||||
let alice = TestContext::builder().configure_alice().build().await;
|
||||
alice.ctx.emit_event(EventType::Info("hello".into()));
|
||||
// panic!("Alice fails");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_with_bob() {
|
||||
let bob = TestContext::builder().configure_bob().build().await;
|
||||
bob.ctx.emit_event(EventType::Info("there".into()));
|
||||
// panic!("Bob fails");
|
||||
}
|
||||
|
||||
#[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;
|
||||
alice.ctx.emit_event(EventType::Info("hello".into()));
|
||||
bob.ctx.emit_event(EventType::Info("there".into()));
|
||||
// panic!("Both fail");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: updated subject\n\
|
||||
Message-ID: <msg2@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -100,14 +100,13 @@ mod tests {
|
||||
\n\
|
||||
second message\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: original subject\n\
|
||||
Message-ID: <msg1@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -115,7 +114,6 @@ mod tests {
|
||||
\n\
|
||||
first message\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -137,7 +135,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <msg1@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde\n\
|
||||
@@ -146,7 +144,6 @@ mod tests {
|
||||
\n\
|
||||
first message\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -157,7 +154,7 @@ mod tests {
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <msg3@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde\n\
|
||||
@@ -167,14 +164,13 @@ mod tests {
|
||||
\n\
|
||||
third message\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <msg2@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde\n\
|
||||
@@ -184,7 +180,6 @@ mod tests {
|
||||
\n\
|
||||
second message\n",
|
||||
"INBOX",
|
||||
3,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
1577
src/webxdc.rs
Normal file
1577
src/webxdc.rs
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user