Compare commits
135 Commits
1.43.0
...
bench-bott
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f69ede0de | ||
|
|
11b9a933b0 | ||
|
|
8d9fa233c5 | ||
|
|
edcad6f5d5 | ||
|
|
23eb3c40ca | ||
|
|
8727e0acf8 | ||
|
|
dd682e87db | ||
|
|
0b743c6bc3 | ||
|
|
8f290530fd | ||
|
|
0f164861c7 | ||
|
|
4481ab18f5 | ||
|
|
927c7eb59d | ||
|
|
6eef4066db | ||
|
|
df8e5f6088 | ||
|
|
1cb4e41883 | ||
|
|
cbfada3e4a | ||
|
|
c19e35b68d | ||
|
|
94e52b5598 | ||
|
|
f6854fd22f | ||
|
|
3fc03fb5ca | ||
|
|
42bd9f71f0 | ||
|
|
064d62e758 | ||
|
|
5a2f0d07a0 | ||
|
|
fd54b6b5b1 | ||
|
|
aae8163696 | ||
|
|
a01755b65b | ||
|
|
6763dd653e | ||
|
|
0fc57bdb35 | ||
|
|
4bd2a9084c | ||
|
|
0816e6d0f6 | ||
|
|
c7d72d64cc | ||
|
|
e33f6c1c85 | ||
|
|
b4c85c534d | ||
|
|
763334d0aa | ||
|
|
be922eef0f | ||
|
|
1325b2f7c6 | ||
|
|
b9ca7b8ace | ||
|
|
3faf968b7c | ||
|
|
1a736ca6c3 | ||
|
|
f1ec1a0765 | ||
|
|
fc2367894b | ||
|
|
a0293de397 | ||
|
|
ed3eabe3e5 | ||
|
|
91a3b1dfbd | ||
|
|
b022ea4f3c | ||
|
|
4b75f3a177 | ||
|
|
af07f947d1 | ||
|
|
d26347af7e | ||
|
|
36927d7c6b | ||
|
|
a3c700ce85 | ||
|
|
0969de5e6e | ||
|
|
cf72d9a41e | ||
|
|
77c61ab25b | ||
|
|
231946646c | ||
|
|
c6dbd9f1a1 | ||
|
|
486ba74f8b | ||
|
|
a9faaa5cbc | ||
|
|
061bee382b | ||
|
|
299c70e1cc | ||
|
|
54edd4d211 | ||
|
|
810bd514d7 | ||
|
|
0bf8017e8f | ||
|
|
8f7f4f95e8 | ||
|
|
9810e5562a | ||
|
|
2feecbc9ff | ||
|
|
55389c4190 | ||
|
|
1c2b4fa7fc | ||
|
|
0208c02ec2 | ||
|
|
8159141d44 | ||
|
|
38a32d176b | ||
|
|
a66d624b87 | ||
|
|
bd0b352854 | ||
|
|
ad13097a9a | ||
|
|
7ffefdff89 | ||
|
|
920753ad50 | ||
|
|
00c1383419 | ||
|
|
526e76c59f | ||
|
|
baec61cc4d | ||
|
|
e3f3602a26 | ||
|
|
6285d18186 | ||
|
|
21f8fefcce | ||
|
|
0c567fefa6 | ||
|
|
b97c334e0c | ||
|
|
dd27929adf | ||
|
|
1ae49c1fca | ||
|
|
4bdcdbb922 | ||
|
|
99ca582e25 | ||
|
|
48e5016abf | ||
|
|
58a8ae1914 | ||
|
|
04629c4b2e | ||
|
|
2550ed3f43 | ||
|
|
f69f5fa259 | ||
|
|
8b22f74fa6 | ||
|
|
fa795c54df | ||
|
|
ffd6877243 | ||
|
|
b3db1a2178 | ||
|
|
4f8e7e0166 | ||
|
|
e081c8b9ff | ||
|
|
9a21d5e9d9 | ||
|
|
1566b7105e | ||
|
|
418b2c0478 | ||
|
|
ca0c8f77a1 | ||
|
|
24d0382ec3 | ||
|
|
6d68fd4500 | ||
|
|
da5796e8a6 | ||
|
|
801b9f3ffa | ||
|
|
2c41b3f3e0 | ||
|
|
ac72280e69 | ||
|
|
528b5e9469 | ||
|
|
ec4d68af2b | ||
|
|
ea0aa4a93f | ||
|
|
b83f3e5ea0 | ||
|
|
6c7d7f0c16 | ||
|
|
1bfc8d0300 | ||
|
|
8faf397af2 | ||
|
|
18d8ef9ffc | ||
|
|
35c250c705 | ||
|
|
a3ecbb3809 | ||
|
|
2a155d4849 | ||
|
|
b1d862bc7d | ||
|
|
a3a78bff8e | ||
|
|
9e8afbb4d4 | ||
|
|
7a7cdad566 | ||
|
|
2b74c58a45 | ||
|
|
1d20ae6801 | ||
|
|
3c8e60a2a3 | ||
|
|
7eb72fea92 | ||
|
|
5bfa82e7ec | ||
|
|
cfd222a109 | ||
|
|
3577491b31 | ||
|
|
d106a027c7 | ||
|
|
dc4fa1de65 | ||
|
|
f480c65071 | ||
|
|
a315f919e4 | ||
|
|
03a4115a52 |
@@ -138,6 +138,12 @@ jobs:
|
||||
- py-docs
|
||||
- wheelhouse
|
||||
|
||||
remote_tests_rust:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- run: ci_scripts/remote_tests_rust.sh
|
||||
|
||||
remote_tests_python:
|
||||
machine: true
|
||||
steps:
|
||||
@@ -172,6 +178,11 @@ workflows:
|
||||
jobs:
|
||||
# - cargo_fetch
|
||||
|
||||
- remote_tests_rust:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
- remote_tests_python:
|
||||
filters:
|
||||
tags:
|
||||
|
||||
22
.github/workflows/ci.yml
vendored
@@ -50,16 +50,16 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
rust: [1.45.0]
|
||||
experimental: [false]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: nightly
|
||||
experimental: true
|
||||
- os: windows-latest
|
||||
rust: nightly
|
||||
experimental: true
|
||||
- os: macOS-latest
|
||||
rust: nightly
|
||||
experimental: true
|
||||
# include:
|
||||
# - os: ubuntu-latest
|
||||
# rust: nightly
|
||||
# experimental: true
|
||||
# - os: windows-latest
|
||||
# rust: nightly
|
||||
# experimental: true
|
||||
# - os: macOS-latest
|
||||
# rust: nightly
|
||||
# experimental: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all --bins --examples --tests
|
||||
args: --all --bins --examples --tests --features repl
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
|
||||
53
CHANGELOG.md
@@ -1,5 +1,58 @@
|
||||
# Changelog
|
||||
|
||||
## 1.45.0
|
||||
|
||||
- add `dc_accounts_t` account manager object and related api functions #1784
|
||||
|
||||
- add capability to import backups as .tar files,
|
||||
which will become the default in a subsequent release #1749
|
||||
|
||||
- try various server domains on configuration #1780 #1838
|
||||
|
||||
- recognize .tgs files as stickers #1826
|
||||
|
||||
- remove X-Mailer debug header #1819
|
||||
|
||||
- improve guessing message types from extension #1818
|
||||
|
||||
- fix showing unprotected subjects in encrypted messages #1822
|
||||
|
||||
- fix threading in interaction with non-delta-clients #1843
|
||||
|
||||
- fix handling if encryption degrades #1829
|
||||
|
||||
- fix webrtc-servers names set by the user #1831
|
||||
|
||||
- update provider database #1828
|
||||
|
||||
- update async-imap to fix Oauth2 #1837
|
||||
|
||||
- optimize jpeg assets with trimage #1840
|
||||
|
||||
- add tests and documentations #1809 #1820
|
||||
|
||||
|
||||
## 1.44.0
|
||||
|
||||
- fix peerstate issues #1800 #1805
|
||||
|
||||
- fix a crash related to muted chats #1803
|
||||
|
||||
- fix incorrect dimensions sometimes reported for images #1806
|
||||
|
||||
- fixed `dc_chat_get_remaining_mute_duration` function #1807
|
||||
|
||||
- handle empty tags (e.g. `<br/>`) in HTML mails #1810
|
||||
|
||||
- always translate the message about disappearing messages timer change #1813
|
||||
|
||||
- improve footer detection in plain text email #1812
|
||||
|
||||
- update device chat icon to fix warnings in iOS logs #1802
|
||||
|
||||
- fix deletion of multiple messages #1795
|
||||
|
||||
|
||||
## 1.43.0
|
||||
|
||||
- improve using own jitsi-servers #1785
|
||||
|
||||
148
Cargo.lock
generated
@@ -17,9 +17,9 @@ checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
|
||||
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
@@ -27,7 +27,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331"
|
||||
dependencies = [
|
||||
"generic-array 0.14.3",
|
||||
"generic-array 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -138,9 +138,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.2.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "958a8af017616083a7c739a9c4da4b757a6816593734b4b6145adbe1421526a5"
|
||||
checksum = "43de69555a39d52918e2bc33a408d3c0a86c829b212d898f4ca25d21a6387478"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener",
|
||||
@@ -149,9 +149,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-h1"
|
||||
version = "2.1.1"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63da30f1df6a14c2e111596727d1e9dbd593a749dd66f0a08da005733e2a880e"
|
||||
checksum = "7ca2b5cfe1804f48bb8dfb1b2391e6e9a3fbf89e07514dce3bddb03eb4d529db"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"byte-pool",
|
||||
@@ -165,9 +165,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.3.3"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c082ecb52ec3af0aa61d4314750086134ef9c7521edb41f7981c0f45650c0c7b"
|
||||
checksum = "caadebe62f5ad78d6418a47a86b5b84439a52bdd5c893e55a5c81974da111455"
|
||||
dependencies = [
|
||||
"async-native-tls",
|
||||
"async-std",
|
||||
@@ -257,6 +257,20 @@ dependencies = [
|
||||
"trust-dns-resolver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-tar"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb619eae01ab289095debb1ff7c02710d5124c20edde1b2eca926572a34c3998"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"filetime",
|
||||
"libc",
|
||||
"pin-project",
|
||||
"redox_syscall",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "3.0.0"
|
||||
@@ -401,7 +415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
|
||||
dependencies = [
|
||||
"block-padding 0.2.0",
|
||||
"generic-array 0.14.3",
|
||||
"generic-array 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -410,7 +424,7 @@ version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10"
|
||||
dependencies = [
|
||||
"generic-array 0.14.3",
|
||||
"generic-array 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -615,9 +629,9 @@ checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "1.1.2"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1582139bb74d97ef232c30bc236646017db06f13ee7cc01fa24c9e55640f86d4"
|
||||
checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3"
|
||||
dependencies = [
|
||||
"cache-padded",
|
||||
]
|
||||
@@ -710,7 +724,7 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
|
||||
dependencies = [
|
||||
"generic-array 0.14.3",
|
||||
"generic-array 0.14.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
@@ -774,9 +788,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.2.1"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72aa14c04dfae8dd7d8a2b1cb7ca2152618cd01336dbfe704b8dcbf8d41dbd69"
|
||||
checksum = "d4d0e2d24e5ee3b23a01de38eefdcd978907890701f08ffffd4cb457ca4ee8d6"
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
@@ -790,7 +804,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.43.0"
|
||||
version = "1.45.0"
|
||||
dependencies = [
|
||||
"ansi_term 0.12.1",
|
||||
"anyhow",
|
||||
@@ -799,6 +813,7 @@ dependencies = [
|
||||
"async-smtp",
|
||||
"async-std",
|
||||
"async-std-resolver",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"backtrace",
|
||||
"base64 0.12.3",
|
||||
@@ -807,6 +822,7 @@ dependencies = [
|
||||
"charset",
|
||||
"chrono",
|
||||
"deltachat_derive",
|
||||
"dirs 3.0.1",
|
||||
"email",
|
||||
"encoded-words",
|
||||
"escaper",
|
||||
@@ -848,7 +864,9 @@ dependencies = [
|
||||
"surf",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -861,7 +879,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.43.0"
|
||||
version = "1.45.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
@@ -869,6 +887,7 @@ dependencies = [
|
||||
"human-panic",
|
||||
"libc",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -930,7 +949,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
|
||||
dependencies = [
|
||||
"generic-array 0.14.3",
|
||||
"generic-array 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -944,6 +963,26 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "discard"
|
||||
version = "1.0.4"
|
||||
@@ -1095,9 +1134,9 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc4bfcfacb61d231109d1d55202c1f33263319668b168843e02ad4652725ec9c"
|
||||
checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -1120,9 +1159,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "error-chain"
|
||||
version = "0.12.2"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd"
|
||||
checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"version_check 0.9.2",
|
||||
@@ -1139,9 +1178,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.2.1"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829694371bd7bbc6aee17c4ff624aad8bf9f4dc06c6f9f6071eaa08c89530d10"
|
||||
checksum = "7f14646a9e0430150a87951622ba9675472b68e384b7701b8423b30560805c7a"
|
||||
|
||||
[[package]]
|
||||
name = "fake-simd"
|
||||
@@ -1172,9 +1211,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.3.3"
|
||||
version = "1.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36a9cb09840f81cd211e435d00a4e487edd263dc3c8ff815c32dd76ad668ebed"
|
||||
checksum = "4bd3bdaaf0a72155260a1c098989b60db1cbb22d6a628e64f16237aa4da93cc7"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed85775dcc68644b5c950ac06a2b23768d3bc9390464151aaf27136998dcf9e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
@@ -1348,9 +1399,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.3"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60fb4bb6bba52f78a471264d9a3b7d026cc0af47b22cd2cffbc0b787ca003e63"
|
||||
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check 0.9.2",
|
||||
@@ -1407,9 +1458,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34f595585f103464d8d2f6e9864682d74c1601fed5e07d62b1c9058dba8246fb"
|
||||
checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25"
|
||||
dependencies = [
|
||||
"autocfg 1.0.0",
|
||||
]
|
||||
@@ -1499,9 +1550,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http-types"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bc341c4b7a71eb0d85c760dc7363648b82ecc38fa5ead50f69b52858df708b9"
|
||||
checksum = "bb4daf8dc001485f4a32a7a17c54c67fa8a10340188f30ba87ac0fe1a9451e97"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
@@ -1590,9 +1641,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b88cd59ee5f71fea89a62248fc8f387d44400cefe05ef548466d61ced9029a7"
|
||||
checksum = "86b45e59b16c76b11bf9738fd5d38879d3bd28ad292d7b313608becb17ae2df9"
|
||||
dependencies = [
|
||||
"autocfg 1.0.0",
|
||||
"hashbrown",
|
||||
@@ -1666,9 +1717,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kamadak-exif"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a5e66d5b5469321038611f7f0e845a48989e4fd54987b6e5bb4c8ae3adbace7"
|
||||
checksum = "524f22a6373f7d4f4e7caa44d54efbaf1e92c09d41f38647db2784ebce610aa8"
|
||||
dependencies = [
|
||||
"mutate_once",
|
||||
]
|
||||
@@ -2194,7 +2245,7 @@ dependencies = [
|
||||
"digest 0.9.0",
|
||||
"ed25519-dalek",
|
||||
"flate2",
|
||||
"generic-array 0.14.3",
|
||||
"generic-array 0.14.4",
|
||||
"hex",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@@ -2618,7 +2669,7 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f47ea1ceb347d2deae482d655dc8eef4bd82363d3329baffa3818bd76fea48b"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"dirs 1.0.5",
|
||||
"libc",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -2991,7 +3042,7 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f8ed9974042b8c3672ff3030a69fcc03b74c47c3d1ecb7755e8a3626011e88"
|
||||
dependencies = [
|
||||
"generic-array 0.14.3",
|
||||
"generic-array 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3046,9 +3097,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.36"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cdb98bcb1f9d81d07b536179c269ea15999b5d14ea958196413869445bb5250"
|
||||
checksum = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3308,7 +3359,7 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402"
|
||||
dependencies = [
|
||||
"generic-array 0.14.3",
|
||||
"generic-array 0.14.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
@@ -3321,6 +3372,7 @@ dependencies = [
|
||||
"idna",
|
||||
"matches",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3336,6 +3388,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3534,6 +3587,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.1.0"
|
||||
|
||||
10
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.43.0"
|
||||
version = "1.45.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -23,7 +23,7 @@ num-traits = "0.2.6"
|
||||
async-smtp = "0.3"
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
async-imap = "0.3.1"
|
||||
async-imap = "0.4.0"
|
||||
async-native-tls = { version = "0.3.3" }
|
||||
async-std = { version = "1.6.1", features = ["unstable"] }
|
||||
base64 = "0.12"
|
||||
@@ -59,11 +59,15 @@ anyhow = "1.0.28"
|
||||
async-trait = "0.1.31"
|
||||
url = "2.1.1"
|
||||
async-std-resolver = "0.19.5"
|
||||
async-tar = "0.3.0"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
log = {version = "0.4.8", optional = true }
|
||||
rustyline = { version = "4.1.0", optional = true }
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
dirs = { version = "3.0.1", optional=true }
|
||||
toml = "0.5.6"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -94,7 +98,7 @@ required-features = ["repl"]
|
||||
[features]
|
||||
default = []
|
||||
internals = []
|
||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term"]
|
||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
|
||||
nightly = ["pgp/nightly"]
|
||||
|
||||
|
||||
@@ -95,7 +95,8 @@ $ cargo build -p deltachat_ffi --release
|
||||
|
||||
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
||||
|
||||
|
||||
- `RUST_LOG=info,async_imap=trace,async_smtp=trace`: enable IMAP and
|
||||
SMTP tracing in addition to info messages.
|
||||
|
||||
### Expensive tests
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 112 KiB |
@@ -30,6 +30,7 @@ set +x
|
||||
|
||||
ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
|
||||
set +x -e
|
||||
shopt -s huponexit
|
||||
cd $BUILDDIR
|
||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
|
||||
@@ -24,6 +24,11 @@ echo "--- Running $CIRCLE_JOB remotely"
|
||||
|
||||
ssh $SSHTARGET <<_HERE
|
||||
set +x -e
|
||||
|
||||
# make sure all processes exit when ssh dies
|
||||
shopt -s huponexit
|
||||
|
||||
export RUSTC_WRAPPER=\`which sccache\`
|
||||
cd $BUILDDIR
|
||||
# let's share the target dir with our last run on this branch/job-type
|
||||
# cargo will make sure to block/unblock us properly
|
||||
|
||||
@@ -20,6 +20,9 @@ echo "--- Running $CIRCLE_JOB remotely"
|
||||
|
||||
ssh $SSHTARGET <<_HERE
|
||||
set +x -e
|
||||
# make sure all processes exit when ssh dies
|
||||
shopt -s huponexit
|
||||
export RUSTC_WRAPPER=\`which sccache\`
|
||||
cd $BUILDDIR
|
||||
# let's share the target dir with our last run on this branch/job-type
|
||||
# cargo will make sure to block/unblock us properly
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.43.0"
|
||||
version = "1.45.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -23,6 +23,7 @@ serde_json = "1.0"
|
||||
async-std = "1.6.0"
|
||||
anyhow = "1.0.28"
|
||||
thiserror = "1.0.14"
|
||||
rand = "0.7.3"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -12,6 +12,7 @@ extern "C" {
|
||||
|
||||
|
||||
typedef struct _dc_context dc_context_t;
|
||||
typedef struct _dc_accounts dc_accounts_t;
|
||||
typedef struct _dc_array dc_array_t;
|
||||
typedef struct _dc_chatlist dc_chatlist_t;
|
||||
typedef struct _dc_chat dc_chat_t;
|
||||
@@ -21,6 +22,7 @@ typedef struct _dc_lot dc_lot_t;
|
||||
typedef struct _dc_provider dc_provider_t;
|
||||
typedef struct _dc_event dc_event_t;
|
||||
typedef struct _dc_event_emitter dc_event_emitter_t;
|
||||
typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
|
||||
|
||||
|
||||
/**
|
||||
@@ -194,6 +196,9 @@ typedef struct _dc_event_emitter dc_event_emitter_t;
|
||||
* @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);
|
||||
|
||||
@@ -205,14 +210,33 @@ dc_context_t* dc_context_new (const char* os_name, const char* d
|
||||
* are closed. You can also do this explicitly by calling dc_close() on your own
|
||||
* before calling dc_context_unref().
|
||||
*
|
||||
* You have to call this function
|
||||
* also for accounts returned by dc_accounts_get_account() or dc_accounts_get_selected_account(),
|
||||
* however, in this case, the context is not shut down but just a reference counter is decreased
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object as created by dc_context_new(),
|
||||
* dc_accounts_get_account() or dc_accounts_get_selected_account().
|
||||
* If NULL is given, nothing is done.
|
||||
* @return None.
|
||||
*/
|
||||
void dc_context_unref (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Get the ID of a context object.
|
||||
* Each context has an ID assigned.
|
||||
* If the context was created through the dc_accounts_t account manager,
|
||||
* the ID is unique, no other context handled by the account manager will have the same ID.
|
||||
* If the context was created by dc_context_new(), a random ID is assigned.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created eg. by dc_accounts_get_account() or dc_context_new().
|
||||
* @return The context-id.
|
||||
*/
|
||||
uint32_t dc_get_id (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Create the event emitter that is used to receive events.
|
||||
* The library will emit various @ref DC_EVENT events as "new message", "message read" etc.
|
||||
@@ -235,7 +259,7 @@ dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
|
||||
* Get the blob directory.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @return Blob directory associated with the context object, empty string if unset or on errors. NULL is never returned.
|
||||
* The returned string must be released using dc_str_unref().
|
||||
*/
|
||||
@@ -250,10 +274,12 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `mail_user` = IMAP-username, guessed if left out
|
||||
* - `mail_pw` = IMAP-password (always needed)
|
||||
* - `mail_port` = IMAP-port, guessed if left out
|
||||
* - `mail_security`= IMAP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
|
||||
* - `send_server` = SMTP-server, guessed if left out
|
||||
* - `send_user` = SMTP-user, guessed if left out
|
||||
* - `send_pw` = SMTP-password, guessed if left out
|
||||
* - `send_port` = SMTP-port, guessed if left out
|
||||
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
|
||||
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
|
||||
* - `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)
|
||||
@@ -328,7 +354,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object
|
||||
* @param context The context object.
|
||||
* @param key The option to change, see above.
|
||||
* @param value The value to save for "key"
|
||||
* @return 0=failure, 1=success
|
||||
@@ -352,7 +378,7 @@ int dc_set_config (dc_context_t* context, const char*
|
||||
* The config-keys are the keys that can be passed to the parameter `key` of this function.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new(). For querying system values, this can be NULL.
|
||||
* @param context The context object. For querying system values, this can be NULL.
|
||||
* @param key The key to query.
|
||||
* @return Returns current value of "key", if "key" is unset, the default
|
||||
* value is returned. The returned value must be released using dc_str_unref(), NULL is never
|
||||
@@ -403,7 +429,7 @@ int dc_set_config_from_qr (dc_context_t* context, const char* qr);
|
||||
* included when however.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @return String which must be released using dc_str_unref() after usage. Never returns NULL.
|
||||
*/
|
||||
char* dc_get_info (dc_context_t* context);
|
||||
@@ -425,7 +451,7 @@ char* dc_get_info (dc_context_t* context);
|
||||
* dc_configure() can be called as usual afterwards.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param addr E-mail address the user has entered.
|
||||
* In case the user selects a different e-mail-address during
|
||||
* authorization, this is corrected in dc_configure()
|
||||
@@ -479,7 +505,7 @@ char* dc_get_oauth2_url (dc_context_t* context, const char*
|
||||
* and parameters as `addr`, `mail_pw` etc. are set to that.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @return None.
|
||||
*
|
||||
* There is no need to call dc_configure() on every program start,
|
||||
@@ -503,7 +529,7 @@ void dc_configure (dc_context_t* context);
|
||||
* to enter some settings and dc_configure() is called in a thread then.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @return 1=context is configured and can be used;
|
||||
* 0=context is not configured and a configuration by dc_configure() is required.
|
||||
*/
|
||||
@@ -515,6 +541,9 @@ int dc_is_configured (const dc_context_t* context);
|
||||
* If IO is already running, nothing happens.
|
||||
* To check the current IO state, use dc_is_io_running().
|
||||
*
|
||||
* If the context was created by the dc_accounts_t account manager,
|
||||
* use dc_accounts_start_io() instead of this function.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @return None
|
||||
@@ -525,7 +554,7 @@ void dc_start_io (dc_context_t* context);
|
||||
* Check if IO (SMTP/IMAP/Jobs) has been started.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @return 1=IO is running;
|
||||
* 0=IO is not running.
|
||||
*/
|
||||
@@ -536,6 +565,9 @@ int dc_is_io_running(const dc_context_t* context);
|
||||
* If IO is not running, nothing happens.
|
||||
* To check the current IO state, use dc_is_io_running().
|
||||
*
|
||||
* If the context was created by the dc_accounts_t account manager,
|
||||
* use dc_accounts_stop_io() instead of this function.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @return None
|
||||
@@ -558,6 +590,9 @@ void dc_stop_io(dc_context_t* context);
|
||||
* and will led to let the jobs fail faster, with fewer retries
|
||||
* and may avoid messages being sent out.
|
||||
*
|
||||
* Finally, if the context was created by the dc_accounts_t account manager,
|
||||
* use dc_accounts_maybe_network() instead of this function.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @return None.
|
||||
@@ -869,7 +904,7 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
||||
* However, UIs might some things differently, eg. play a different sound.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param context The context object
|
||||
* @param chat_id The chat to start a videochat for.
|
||||
* @return The id if the message sent out
|
||||
* or 0 for errors.
|
||||
@@ -894,7 +929,7 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
* If the draft is modified, an #DC_EVENT_MSGS_CHANGED will be sent.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat ID to save the draft for.
|
||||
* @param msg The message to save as a draft.
|
||||
* Existing draft will be overwritten.
|
||||
@@ -919,7 +954,7 @@ void dc_set_draft (dc_context_t* context, uint32_t ch
|
||||
* To check, if a given chat is a device-chat, see dc_chat_is_device_talk()
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param label A unique name for the message to add.
|
||||
* The label is typically not displayed to the user and
|
||||
* must be created from the characters `A-Z`, `a-z`, `0-9`, `_` or `-`.
|
||||
@@ -968,7 +1003,7 @@ uint32_t dc_add_device_msg (dc_context_t* context, const char*
|
||||
* (however, not seen device messages are added and may re-create the device-chat).
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @return None.
|
||||
*/
|
||||
void dc_update_device_chats (dc_context_t* context);
|
||||
@@ -979,7 +1014,7 @@ void dc_update_device_chats (dc_context_t* context);
|
||||
* Device-messages can be added dc_add_device_msg().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param label Label of the message to check.
|
||||
* @return 1=A message with this label was added at some point,
|
||||
* 0=A message with this label was never added.
|
||||
@@ -992,7 +1027,7 @@ int dc_was_device_msg_ever_added (dc_context_t* context, const char*
|
||||
* See dc_set_draft() for more details about drafts.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat ID to get the draft for.
|
||||
* @return Message object.
|
||||
* Can be passed directly to dc_send_msg().
|
||||
@@ -1219,7 +1254,7 @@ dc_array_t* dc_get_chat_contacts (dc_context_t* context, uint32_t ch
|
||||
* Get the chat's ephemeral message timer.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat ID.
|
||||
*
|
||||
* @return ephemeral timer value in seconds, 0 if the timer is disabled or if there is an error
|
||||
@@ -1280,7 +1315,7 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
|
||||
* This may be useful if you want to show some help for just created groups.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param verified If set to 1 the function creates a secure verified group.
|
||||
* Only secure-verified members are allowed in these groups
|
||||
* and end-to-end-encryption is always enabled.
|
||||
@@ -1296,7 +1331,7 @@ uint32_t dc_create_group_chat (dc_context_t* context, int verifie
|
||||
* Check if a given contact ID is a member of a group chat.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat ID to check.
|
||||
* @param contact_id The contact ID to check. To check if yourself is member
|
||||
* of the chat, pass DC_CONTACT_ID_SELF (1) here.
|
||||
@@ -1316,7 +1351,7 @@ int dc_is_contact_in_chat (dc_context_t* context, uint32_t ch
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat ID to add the contact to. Must be a group chat.
|
||||
* @param contact_id The contact ID to add to the chat.
|
||||
* @return 1=member added to group, 0=error
|
||||
@@ -1333,7 +1368,7 @@ int dc_add_contact_to_chat (dc_context_t* context, uint32_t ch
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat ID to remove the contact from. Must be a group chat.
|
||||
* @param contact_id The contact ID to remove from the chat.
|
||||
* @return 1=member removed from group, 0=error
|
||||
@@ -1352,7 +1387,7 @@ int dc_remove_contact_from_chat (dc_context_t* context, uint32_t ch
|
||||
* @memberof dc_context_t
|
||||
* @param chat_id The chat ID to set the name for. Must be a group chat.
|
||||
* @param name New name of the group.
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_set_chat_name (dc_context_t* context, uint32_t chat_id, const char* name);
|
||||
@@ -1365,7 +1400,7 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
|
||||
* participating in a chat.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat ID to set the ephemeral message timer for.
|
||||
* @param timer The timer value in seconds or 0 to disable the timer.
|
||||
*
|
||||
@@ -1384,7 +1419,7 @@ int dc_set_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id, uint32
|
||||
* To find out the profile image of a chat, use dc_chat_get_profile_image()
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param chat_id The chat ID to set the image for.
|
||||
* @param image Full path of the image to use as the group image. The image will immediately be copied to the
|
||||
* `blobdir`; the original image will not be needed anymore.
|
||||
@@ -1406,7 +1441,7 @@ int dc_set_chat_profile_image (dc_context_t* context, uint32_t ch
|
||||
* @memberof dc_context_t
|
||||
* @param chat_id The chat ID to set the mute duration.
|
||||
* @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds)
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_set_chat_mute_duration (dc_context_t* context, uint32_t chat_id, int64_t duration);
|
||||
@@ -1421,7 +1456,7 @@ int dc_set_chat_mute_duration (dc_context_t* context, ui
|
||||
* max. text returned by dc_msg_get_text() (about 30000 characters).
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object object.
|
||||
* @param msg_id The message id for which information should be generated
|
||||
* @return Text string, must be released using dc_str_unref() after usage
|
||||
*/
|
||||
@@ -1435,7 +1470,7 @@ char* dc_get_msg_info (dc_context_t* context, uint32_t ms
|
||||
* was called before.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param msg_id The message id, must be the id of an incoming message.
|
||||
* @return Raw headers as a multi-line string, must be released using dc_str_unref() after usage.
|
||||
* Returns NULL if there are no headers saved for the given message,
|
||||
@@ -1450,7 +1485,7 @@ char* dc_get_mime_headers (dc_context_t* context, uint32_t ms
|
||||
* on the IMAP server.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new()
|
||||
* @param context The context object
|
||||
* @param msg_ids an array of uint32_t containing all message IDs that should be deleted
|
||||
* @param msg_cnt The number of messages IDs in the msg_ids array
|
||||
* @return None.
|
||||
@@ -1462,7 +1497,7 @@ void dc_delete_msgs (dc_context_t* context, const uint3
|
||||
* Deprecated, use dc_set_config() with the key "delete_server_after" instead.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new()
|
||||
* @param context The context object.
|
||||
* @param flags What to delete, a combination of the @ref DC_EMPTY flags
|
||||
* @return None.
|
||||
*/
|
||||
@@ -1473,7 +1508,7 @@ void dc_empty_server (dc_context_t* context, uint32_t fl
|
||||
* Forward messages to another chat.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new()
|
||||
* @param context The context object.
|
||||
* @param msg_ids An array of uint32_t containing all message IDs that should be forwarded
|
||||
* @param msg_cnt The number of messages IDs in the msg_ids array
|
||||
* @param chat_id The destination chat ID.
|
||||
@@ -1490,7 +1525,7 @@ void dc_forward_msgs (dc_context_t* context, const uint3
|
||||
* Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new()
|
||||
* @param context The context object.
|
||||
* @param contact_id The contact ID of which all messages should be marked as noticed.
|
||||
* @return None.
|
||||
*/
|
||||
@@ -1518,7 +1553,7 @@ void dc_markseen_msgs (dc_context_t* context, const uint3
|
||||
* dc_get_chat_msgs() using the chat_id DC_CHAT_ID_STARRED.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new()
|
||||
* @param context The context object.
|
||||
* @param msg_ids An array of uint32_t message IDs defining the messages to star or unstar
|
||||
* @param msg_cnt The number of IDs in msg_ids
|
||||
* @param star 0=unstar the messages in msg_ids, 1=star them
|
||||
@@ -1533,7 +1568,7 @@ void dc_star_msgs (dc_context_t* context, const uint3
|
||||
* For a list or chats, see dc_get_chatlist()
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param msg_id The message ID for which the message object should be created.
|
||||
* @return A dc_msg_t message object.
|
||||
* On errors, NULL is returned.
|
||||
@@ -1568,7 +1603,7 @@ int dc_may_be_valid_addr (const char* addr);
|
||||
* use dc_may_be_valid_addr().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param addr The e-mail-address to check.
|
||||
* @return Contact ID of the contact belonging to the e-mail-address
|
||||
* or 0 if there is no contact that is or was introduced by an accepted contact.
|
||||
@@ -1588,7 +1623,7 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
|
||||
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param name Name of the contact to add. If you do not know the name belonging
|
||||
* to the address, you can give NULL here.
|
||||
* @param addr E-mail-address of the contact to add. If the email address
|
||||
@@ -1619,7 +1654,7 @@ uint32_t dc_create_contact (dc_context_t* context, const char*
|
||||
* however, for adding a bunch of addresses, this function is _much_ faster.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context the context object as created by dc_context_new().
|
||||
* @param context the context object.
|
||||
* @param addr_book A multi-line string in the format
|
||||
* `Name one\nAddress one\nName two\nAddress two`.
|
||||
* If an email address already exists, the name is updated
|
||||
@@ -1635,7 +1670,7 @@ int dc_add_address_book (dc_context_t* context, const char*
|
||||
* To get information about a single contact, see dc_get_contact().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param flags A combination of flags:
|
||||
* - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
|
||||
* - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned.
|
||||
@@ -1652,7 +1687,7 @@ dc_array_t* dc_get_contacts (dc_context_t* context, uint32_t fl
|
||||
* Get the number of blocked contacts.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @return The number of blocked contacts.
|
||||
*/
|
||||
int dc_get_blocked_cnt (dc_context_t* context);
|
||||
@@ -1662,7 +1697,7 @@ int dc_get_blocked_cnt (dc_context_t* context);
|
||||
* Get blocked contacts.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @return An array containing all blocked contact IDs. Must be dc_array_unref()'d
|
||||
* after usage.
|
||||
*/
|
||||
@@ -1674,7 +1709,7 @@ dc_array_t* dc_get_blocked_contacts (dc_context_t* context);
|
||||
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param contact_id The ID of the contact to block or unblock.
|
||||
* @param block 1=block contact, 0=unblock contact
|
||||
* @return None.
|
||||
@@ -1688,7 +1723,7 @@ void dc_block_contact (dc_context_t* context, uint32_t co
|
||||
* fingerprint of the contact, used eg. to compare the fingerprints for a simple out-of-band verification.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param contact_id ID of the contact to get the encryption info for.
|
||||
* @return Multi-line text, must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
@@ -1702,7 +1737,7 @@ char* dc_get_contact_encrinfo (dc_context_t* context, uint32_t co
|
||||
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param contact_id ID of the contact to delete.
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
@@ -1717,7 +1752,7 @@ int dc_delete_contact (dc_context_t* context, uint32_t co
|
||||
* defined by dc_set_config().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param contact_id ID of the contact to get the object for.
|
||||
* @return The contact object, must be freed using dc_contact_unref() when no
|
||||
* longer used. NULL on errors.
|
||||
@@ -1740,8 +1775,8 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
||||
* - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`.
|
||||
* 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>.bak`, if more than one backup is create on a day,
|
||||
* the format is `delta-chat.<day>-<number>.bak`
|
||||
* 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
|
||||
@@ -1768,7 +1803,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
||||
* To cancel an import-/export-progress, use dc_stop_ongoing_process().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context.
|
||||
* @param what One of the DC_IMEX_* constants.
|
||||
* @param param1 Meaning depends on the DC_IMEX_* constants. If this parameter is a directory, it should not end with
|
||||
* a slash (otherwise you'll get double slashes when receiving #DC_EVENT_IMEX_FILE_WRITTEN). Set to NULL if not used.
|
||||
@@ -1821,7 +1856,7 @@ void dc_imex (dc_context_t* context, int what, c
|
||||
* ~~~
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param dir Directory to search backups in.
|
||||
* @return String with the backup file, typically given to dc_imex(),
|
||||
* returned strings must be released using dc_str_unref().
|
||||
@@ -2188,6 +2223,239 @@ void dc_delete_all_locations (dc_context_t* context);
|
||||
void dc_str_unref (char* str);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_accounts_t
|
||||
*
|
||||
* This class provides functionality that can be used to
|
||||
* manage several dc_context_t objects running at the same time.
|
||||
* The account manager takes a directory where all
|
||||
* context-databases are created in.
|
||||
*
|
||||
* You can add, remove, import account to the account manager,
|
||||
* all context-databases are persisted and stay available once the
|
||||
* account manager is created again for the same directory.
|
||||
*
|
||||
* All accounts may receive messages at the same time (eg. by #DC_EVENT_INCOMING_MSG),
|
||||
* and all accounts may be accessed by their own dc_context_t object.
|
||||
*
|
||||
* 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_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()
|
||||
*
|
||||
* Additionally, there are functions to list, import and migrate accounts
|
||||
* and to handle a "selected" account, see below.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a new account manager.
|
||||
* The account manager takes an directory
|
||||
* where all context-databases are placed in.
|
||||
* To add a context to the account manager,
|
||||
* use dc_accounts_add_account(), dc_accounts_import_account or dc_accounts_migrate_account().
|
||||
* All account information are persisted.
|
||||
* To remove a context from the account manager,
|
||||
* use dc_accounts_remove_account().
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param os_name
|
||||
* @param dir The directory to create the context-databases in.
|
||||
* If the directory does not exist,
|
||||
* dc_accounts_new() will try to create it.
|
||||
* @return An account manager object.
|
||||
* The object must be passed to the other account manager functions
|
||||
* and must be freed using dc_accounts_unref() after usage.
|
||||
* On errors, NULL is returned.
|
||||
*/
|
||||
dc_accounts_t* dc_accounts_new (const char* os_name, const char* dir);
|
||||
|
||||
|
||||
/**
|
||||
* Free an account manager object.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
*/
|
||||
void dc_accounts_unref (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Add a new account to the account manager.
|
||||
* Internally, dc_context_new() is called using a unique database-name
|
||||
* in the directory specified at dc_accounts_new().
|
||||
*
|
||||
* If the function succceeds,
|
||||
* 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_account (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Import a tarfile-backup to the account manager.
|
||||
* On success, a new account is added to the account-manager,
|
||||
* with all the data provided by the backup-file.
|
||||
* Moreover, the newly created account will be the selected one.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
* @param tarfile Backup as created by dc_imex().
|
||||
* @return Account-id, use dc_accounts_get_account() to get the context object.
|
||||
* On errors, 0 is returned.
|
||||
*/
|
||||
uint32_t dc_accounts_import_account (dc_accounts_t* accounts, const char* tarfile);
|
||||
|
||||
|
||||
/**
|
||||
* Migrate independent accounts into accounts managed by the account mangager.
|
||||
* This will _move_ the database-file and all blob-files to the directory managed
|
||||
* by the account-manager
|
||||
* (to save disk-space on small devices, the files are not _copied_
|
||||
* Once the migration is done, the original file is no longer existent).
|
||||
* Moreover, the newly created account will be the selected one.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
* @param dbfile Unmanaged database-file that was created at some point using dc_context_new().
|
||||
* @return Account-id, use dc_accounts_get_account() to get the context object.
|
||||
* On errors, 0 is returned.
|
||||
*/
|
||||
uint32_t dc_accounts_migrate_account (dc_accounts_t* accounts, const char* dbfile);
|
||||
|
||||
|
||||
/**
|
||||
* Remove an account from the account manager.
|
||||
* This also removes the database-file and all blobs physically.
|
||||
* If the removed account is the selected account,
|
||||
* one of the other accounts will be selected.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_accounts_remove_account (dc_accounts_t* accounts, uint32_t);
|
||||
|
||||
|
||||
/**
|
||||
* List all accounts.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
* @return An array containing all account-ids,
|
||||
* use dc_array_get_id() to get the ids.
|
||||
*/
|
||||
dc_array_t* dc_accounts_get_all (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Get an account-context from an account-id.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
* @param account_id The account-id as returned eg. by dc_accounts_get_all() or dc_accounts_add_account().
|
||||
* @return The account-context, this can be used most similar as a normal,
|
||||
* unmanaged account-context as created by dc_context_new().
|
||||
* Once you do no longer need the context-object, you have to call dc_context_unref() on it,
|
||||
* which, however, will not close the account but only decrease a reference counter.
|
||||
*/
|
||||
dc_context_t* dc_accounts_get_account (dc_accounts_t* accounts, uint32_t account_id);
|
||||
|
||||
|
||||
/**
|
||||
* Get the currently selected account.
|
||||
* If there is at least once account in the account-manager,
|
||||
* there is always a selected one.
|
||||
* To change the selected account, use dc_accounts_select_account();
|
||||
* also adding/importing/migrating accounts may change the selection.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
* @return The account-context, this can be used most similar as a normal,
|
||||
* unmanaged account-context as created by dc_context_new().
|
||||
* Once you do no longer need the context-object, you have to call dc_context_unref() on it,
|
||||
* which, however, will not close the account but only decrease a reference counter.
|
||||
*/
|
||||
dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Change the selected account.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
* @param account_id The account-id as returned eg. by dc_accounts_get_all() or dc_accounts_add_account().
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_accounts_select_account (dc_accounts_t* accounts, uint32_t account_id);
|
||||
|
||||
|
||||
/**
|
||||
* Start job and IMAP/SMTP tasks for all accounts managed by the account manager.
|
||||
* If IO is already running, nothing happens.
|
||||
* This is similar to dc_start_io(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
* @return None.
|
||||
*/
|
||||
void dc_accounts_start_io (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Stop job and IMAP/SMTP tasks for all accounts and return when they are finished.
|
||||
* If IO is not running, nothing happens.
|
||||
* This is similar to dc_stop_io(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
*/
|
||||
void dc_accounts_stop_io (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* This function should be called when there is a hint
|
||||
* that the network is available again.
|
||||
* This is similar to dc_maybe_network(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
*/
|
||||
void dc_accounts_maybe_network (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* Create the event emitter that is used to receive events.
|
||||
*
|
||||
* The library will emit various @ref DC_EVENT events as "new message", "message read" etc.
|
||||
* To get these events, you have to create an event emitter using this function
|
||||
* and call dc_accounts_get_next_event() on the emitter.
|
||||
*
|
||||
* This is similar to dc_get_event_emitter(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts Account manager as created by dc_accounts_new().
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_accounts_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per account manager.
|
||||
* Having more than one event emitter running at the same time on the same account manager
|
||||
* will result in events randomly delivered to the one or to the other.
|
||||
*/
|
||||
dc_accounts_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_array_t
|
||||
*
|
||||
@@ -2489,7 +2757,7 @@ dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, siz
|
||||
* use dc_chatlist_get_summary() in this case instead.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new()
|
||||
* @param context The context object.
|
||||
* @param chat_id Chat to get a summary for.
|
||||
* @param msg_id Messasge to get a summary for.
|
||||
* @return The summary as an dc_lot_t object, see dc_chatlist_get_summary() for details.
|
||||
@@ -3641,7 +3909,7 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
||||
* The provider is extracted from the email address and it's information is returned.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param context The context object as created by dc_context_new().
|
||||
* @param context The context object.
|
||||
* @param email The user's email address to extract the provider info form.
|
||||
* @return a dc_provider_t struct which can be used with the dc_provider_get_*
|
||||
* accessor functions. If no provider info is found, NULL will be
|
||||
@@ -3901,6 +4169,44 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*/
|
||||
#define DC_MSG_VIDEOCHAT_INVITATION 70
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_SOCKET DC_SOCKET
|
||||
*
|
||||
* These constants configure socket security.
|
||||
* To set socket security, use dc_set_config() with the keys "mail_security" and/or "send_security".
|
||||
* If no socket-configuration is explicitly specified, #DC_SOCKET_AUTO is used.
|
||||
*
|
||||
* @addtogroup DC_SOCKET
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Choose socket security automatically.
|
||||
*/
|
||||
#define DC_SOCKET_AUTO 0
|
||||
|
||||
|
||||
/**
|
||||
* Connect via SSL/TLS.
|
||||
*/
|
||||
#define DC_SOCKET_SSL 1
|
||||
|
||||
|
||||
/**
|
||||
* Connect via STARTTLS.
|
||||
*/
|
||||
#define DC_SOCKET_STARTTLS 2
|
||||
|
||||
|
||||
/**
|
||||
* Connect unencrypted, this should not be used.
|
||||
*/
|
||||
#define DC_SOCKET_PLAIN 3
|
||||
|
||||
/**
|
||||
* @}
|
||||
@@ -3935,54 +4241,11 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_LP_AUTH_NORMAL 0x4
|
||||
|
||||
|
||||
/**
|
||||
* Connect to IMAP via STARTTLS.
|
||||
* If this flag is set, automatic configuration is skipped.
|
||||
*/
|
||||
#define DC_LP_IMAP_SOCKET_STARTTLS 0x100
|
||||
|
||||
|
||||
/**
|
||||
* Connect to IMAP via SSL.
|
||||
* If this flag is set, automatic configuration is skipped.
|
||||
*/
|
||||
#define DC_LP_IMAP_SOCKET_SSL 0x200
|
||||
|
||||
|
||||
/**
|
||||
* Connect to IMAP unencrypted, this should not be used.
|
||||
* If this flag is set, automatic configuration is skipped.
|
||||
*/
|
||||
#define DC_LP_IMAP_SOCKET_PLAIN 0x400
|
||||
|
||||
|
||||
/**
|
||||
* Connect to SMTP via STARTTLS.
|
||||
* If this flag is set, automatic configuration is skipped.
|
||||
*/
|
||||
#define DC_LP_SMTP_SOCKET_STARTTLS 0x10000
|
||||
|
||||
|
||||
/**
|
||||
* Connect to SMTP via SSL.
|
||||
* If this flag is set, automatic configuration is skipped.
|
||||
*/
|
||||
#define DC_LP_SMTP_SOCKET_SSL 0x20000
|
||||
|
||||
|
||||
/**
|
||||
* Connect to SMTP unencrypted, this should not be used.
|
||||
* If this flag is set, automatic configuration is skipped.
|
||||
*/
|
||||
#define DC_LP_SMTP_SOCKET_PLAIN 0x40000 ///<
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
#define DC_LP_AUTH_FLAGS (DC_LP_AUTH_OAUTH2|DC_LP_AUTH_NORMAL) // if none of these flags are set, the default is chosen
|
||||
#define DC_LP_IMAP_SOCKET_FLAGS (DC_LP_IMAP_SOCKET_STARTTLS|DC_LP_IMAP_SOCKET_SSL|DC_LP_IMAP_SOCKET_PLAIN) // if none of these flags are set, the default is chosen
|
||||
#define DC_LP_SMTP_SOCKET_FLAGS (DC_LP_SMTP_SOCKET_STARTTLS|DC_LP_SMTP_SOCKET_SSL|DC_LP_SMTP_SOCKET_PLAIN) // if none of these flags are set, the default is chosen
|
||||
|
||||
/**
|
||||
* @defgroup DC_CERTCK DC_CERTCK
|
||||
@@ -4025,12 +4288,14 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
/**
|
||||
* @class dc_event_emitter_t
|
||||
*
|
||||
* Opaque object that is used to get events.
|
||||
* Opaque object that is used to get events from a single context.
|
||||
* You can get an event emitter from a context using dc_get_event_emitter().
|
||||
* If you are using the dc_accounts_t account manager,
|
||||
* dc_accounts_event_emitter_t must be used instead.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the next event from an event emitter object.
|
||||
* Get the next event from a context event emitter object.
|
||||
*
|
||||
* @memberof dc_event_emitter_t
|
||||
* @param emitter Event emitter object as returned from dc_get_event_emitter().
|
||||
@@ -4044,7 +4309,7 @@ dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
|
||||
|
||||
|
||||
/**
|
||||
* Free an event emitter object.
|
||||
* Free a context event emitter object.
|
||||
*
|
||||
* @memberof dc_event_emitter_t
|
||||
* @param emitter Event emitter object as returned from dc_get_event_emitter().
|
||||
@@ -4054,6 +4319,40 @@ dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
|
||||
void dc_event_emitter_unref(dc_event_emitter_t* emitter);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_accounts_event_emitter_t
|
||||
*
|
||||
* Opaque object that is used to get events from the dc_accounts_t account manager.
|
||||
* You get an event emitter from the account manager using dc_accounts_get_event_emitter().
|
||||
* If you are not using the dc_accounts_t account manager but just a single dc_context_t object,
|
||||
* dc_event_emitter_t must be used instead.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the next event from an accounts event emitter object.
|
||||
*
|
||||
* @memberof dc_accounts_event_emitter_t
|
||||
* @param emitter Event emitter object as returned from dc_accounts_get_event_emitter().
|
||||
* @return An event as an dc_event_t object.
|
||||
* You can query the event for information using dc_event_get_id(), dc_event_get_data1_int() and so on;
|
||||
* if you are done with the event, you have to free the event using dc_event_unref().
|
||||
* If NULL is returned, the context belonging to the event emitter is unref'd and the no more events will come;
|
||||
* in this case, free the event emitter using dc_accounts_event_emitter_unref().
|
||||
*/
|
||||
dc_event_t* dc_accounts_get_next_event (dc_accounts_event_emitter_t* emitter);
|
||||
|
||||
|
||||
/**
|
||||
* Free an accounts event emitter object.
|
||||
*
|
||||
* @memberof dc_accounts_event_emitter_t
|
||||
* @param emitter Event emitter object as returned from dc_accounts_get_event_emitter().
|
||||
* If NULL is given, nothing is done and an error is logged.
|
||||
* @return None.
|
||||
*/
|
||||
void dc_accounts_event_emitter_unref(dc_accounts_event_emitter_t* emitter);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_event_t
|
||||
*
|
||||
@@ -4118,6 +4417,18 @@ int dc_event_get_data2_int(dc_event_t* event);
|
||||
char* dc_event_get_data2_str(dc_event_t* event);
|
||||
|
||||
|
||||
/**
|
||||
* Get account-id this event belongs to.
|
||||
* The account-id is of interest only when using the dc_accounts_t account manager.
|
||||
* To get the context object belonging to the event, use dc_accounts_get_account().
|
||||
*
|
||||
* @memberof dc_event_t
|
||||
* @param event Event object as returned from dc_accounts_get_next_event().
|
||||
* @return account-id belonging to the event or 0 for errors.
|
||||
*/
|
||||
uint32_t dc_event_get_account_id(dc_event_t* event);
|
||||
|
||||
|
||||
/**
|
||||
* Free memory used by an event object.
|
||||
* If you forget to do this for an event, this will result in memory leakage.
|
||||
|
||||
@@ -24,6 +24,7 @@ use std::time::{Duration, SystemTime};
|
||||
use async_std::task::{block_on, spawn};
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
|
||||
use deltachat::accounts::Accounts;
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, Origin};
|
||||
@@ -75,13 +76,17 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
};
|
||||
|
||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||
block_on(Context::new(os_name, as_path(dbfile).to_path_buf().into()))
|
||||
} else {
|
||||
block_on(Context::with_blobdir(
|
||||
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(),
|
||||
as_path(blobdir).to_path_buf().into(),
|
||||
id,
|
||||
))
|
||||
} else {
|
||||
eprintln!("blobdir can not be defined explicitly anymore");
|
||||
return ptr::null_mut();
|
||||
};
|
||||
match ctx {
|
||||
Ok(ctx) => Box::into_raw(Box::new(ctx)),
|
||||
@@ -301,6 +306,16 @@ pub unsafe extern "C" fn dc_is_io_running(context: *mut dc_context_t) -> libc::c
|
||||
block_on(ctx.is_io_running()) as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_id(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
ctx.get_id() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub type dc_event_t = Event;
|
||||
|
||||
@@ -332,38 +347,38 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
return 0;
|
||||
}
|
||||
|
||||
let event = &*event;
|
||||
let event = &(*event).typ;
|
||||
match event {
|
||||
Event::Info(_)
|
||||
| Event::SmtpConnected(_)
|
||||
| Event::ImapConnected(_)
|
||||
| Event::SmtpMessageSent(_)
|
||||
| Event::ImapMessageDeleted(_)
|
||||
| Event::ImapMessageMoved(_)
|
||||
| Event::ImapFolderEmptied(_)
|
||||
| Event::NewBlobFile(_)
|
||||
| Event::DeletedBlobFile(_)
|
||||
| Event::Warning(_)
|
||||
| Event::Error(_)
|
||||
| Event::ErrorNetwork(_)
|
||||
| Event::ErrorSelfNotInGroup(_) => 0,
|
||||
Event::MsgsChanged { chat_id, .. }
|
||||
| Event::IncomingMsg { chat_id, .. }
|
||||
| Event::MsgDelivered { chat_id, .. }
|
||||
| Event::MsgFailed { chat_id, .. }
|
||||
| Event::MsgRead { chat_id, .. }
|
||||
| Event::ChatModified(chat_id)
|
||||
| Event::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
|
||||
EventType::Info(_)
|
||||
| EventType::SmtpConnected(_)
|
||||
| EventType::ImapConnected(_)
|
||||
| EventType::SmtpMessageSent(_)
|
||||
| EventType::ImapMessageDeleted(_)
|
||||
| EventType::ImapMessageMoved(_)
|
||||
| EventType::ImapFolderEmptied(_)
|
||||
| EventType::NewBlobFile(_)
|
||||
| EventType::DeletedBlobFile(_)
|
||||
| EventType::Warning(_)
|
||||
| EventType::Error(_)
|
||||
| EventType::ErrorNetwork(_)
|
||||
| EventType::ErrorSelfNotInGroup(_) => 0,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
| EventType::MsgDelivered { chat_id, .. }
|
||||
| EventType::MsgFailed { chat_id, .. }
|
||||
| EventType::MsgRead { chat_id, .. }
|
||||
| EventType::ChatModified(chat_id)
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
|
||||
let id = id.unwrap_or_default();
|
||||
id as libc::c_int
|
||||
}
|
||||
Event::ConfigureProgress(progress) | Event::ImexProgress(progress) => {
|
||||
EventType::ConfigureProgress(progress) | EventType::ImexProgress(progress) => {
|
||||
*progress as libc::c_int
|
||||
}
|
||||
Event::ImexFileWritten(_) => 0,
|
||||
Event::SecurejoinInviterProgress { contact_id, .. }
|
||||
| Event::SecurejoinJoinerProgress { contact_id, .. } => *contact_id as libc::c_int,
|
||||
EventType::ImexFileWritten(_) => 0,
|
||||
EventType::SecurejoinInviterProgress { contact_id, .. }
|
||||
| EventType::SecurejoinJoinerProgress { contact_id, .. } => *contact_id as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,36 +389,36 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
return 0;
|
||||
}
|
||||
|
||||
let event = &*event;
|
||||
let event = &(*event).typ;
|
||||
|
||||
match event {
|
||||
Event::Info(_)
|
||||
| Event::SmtpConnected(_)
|
||||
| Event::ImapConnected(_)
|
||||
| Event::SmtpMessageSent(_)
|
||||
| Event::ImapMessageDeleted(_)
|
||||
| Event::ImapMessageMoved(_)
|
||||
| Event::ImapFolderEmptied(_)
|
||||
| Event::NewBlobFile(_)
|
||||
| Event::DeletedBlobFile(_)
|
||||
| Event::Warning(_)
|
||||
| Event::Error(_)
|
||||
| Event::ErrorNetwork(_)
|
||||
| Event::ErrorSelfNotInGroup(_)
|
||||
| Event::ContactsChanged(_)
|
||||
| Event::LocationChanged(_)
|
||||
| Event::ConfigureProgress(_)
|
||||
| Event::ImexProgress(_)
|
||||
| Event::ImexFileWritten(_)
|
||||
| Event::ChatModified(_) => 0,
|
||||
Event::MsgsChanged { msg_id, .. }
|
||||
| Event::IncomingMsg { msg_id, .. }
|
||||
| Event::MsgDelivered { msg_id, .. }
|
||||
| Event::MsgFailed { msg_id, .. }
|
||||
| Event::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
Event::SecurejoinInviterProgress { progress, .. }
|
||||
| Event::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||
Event::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||
EventType::Info(_)
|
||||
| EventType::SmtpConnected(_)
|
||||
| EventType::ImapConnected(_)
|
||||
| EventType::SmtpMessageSent(_)
|
||||
| EventType::ImapMessageDeleted(_)
|
||||
| EventType::ImapMessageMoved(_)
|
||||
| EventType::ImapFolderEmptied(_)
|
||||
| EventType::NewBlobFile(_)
|
||||
| EventType::DeletedBlobFile(_)
|
||||
| EventType::Warning(_)
|
||||
| EventType::Error(_)
|
||||
| EventType::ErrorNetwork(_)
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::ContactsChanged(_)
|
||||
| EventType::LocationChanged(_)
|
||||
| EventType::ConfigureProgress(_)
|
||||
| EventType::ImexProgress(_)
|
||||
| EventType::ImexFileWritten(_)
|
||||
| EventType::ChatModified(_) => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
| EventType::MsgDelivered { msg_id, .. }
|
||||
| EventType::MsgFailed { msg_id, .. }
|
||||
| EventType::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::SecurejoinInviterProgress { progress, .. }
|
||||
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,45 +429,55 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let event = &*event;
|
||||
let event = &(*event).typ;
|
||||
|
||||
match event {
|
||||
Event::Info(msg)
|
||||
| Event::SmtpConnected(msg)
|
||||
| Event::ImapConnected(msg)
|
||||
| Event::SmtpMessageSent(msg)
|
||||
| Event::ImapMessageDeleted(msg)
|
||||
| Event::ImapMessageMoved(msg)
|
||||
| Event::ImapFolderEmptied(msg)
|
||||
| Event::NewBlobFile(msg)
|
||||
| Event::DeletedBlobFile(msg)
|
||||
| Event::Warning(msg)
|
||||
| Event::Error(msg)
|
||||
| Event::ErrorNetwork(msg)
|
||||
| Event::ErrorSelfNotInGroup(msg) => {
|
||||
EventType::Info(msg)
|
||||
| EventType::SmtpConnected(msg)
|
||||
| EventType::ImapConnected(msg)
|
||||
| EventType::SmtpMessageSent(msg)
|
||||
| EventType::ImapMessageDeleted(msg)
|
||||
| EventType::ImapMessageMoved(msg)
|
||||
| EventType::ImapFolderEmptied(msg)
|
||||
| EventType::NewBlobFile(msg)
|
||||
| EventType::DeletedBlobFile(msg)
|
||||
| EventType::Warning(msg)
|
||||
| EventType::Error(msg)
|
||||
| EventType::ErrorNetwork(msg)
|
||||
| EventType::ErrorSelfNotInGroup(msg) => {
|
||||
let data2 = msg.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
Event::MsgsChanged { .. }
|
||||
| Event::IncomingMsg { .. }
|
||||
| Event::MsgDelivered { .. }
|
||||
| Event::MsgFailed { .. }
|
||||
| Event::MsgRead { .. }
|
||||
| Event::ChatModified(_)
|
||||
| Event::ContactsChanged(_)
|
||||
| Event::LocationChanged(_)
|
||||
| Event::ConfigureProgress(_)
|
||||
| Event::ImexProgress(_)
|
||||
| Event::SecurejoinInviterProgress { .. }
|
||||
| Event::SecurejoinJoinerProgress { .. }
|
||||
| Event::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
Event::ImexFileWritten(file) => {
|
||||
EventType::MsgsChanged { .. }
|
||||
| EventType::IncomingMsg { .. }
|
||||
| EventType::MsgDelivered { .. }
|
||||
| EventType::MsgFailed { .. }
|
||||
| EventType::MsgRead { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ContactsChanged(_)
|
||||
| EventType::LocationChanged(_)
|
||||
| EventType::ConfigureProgress(_)
|
||||
| EventType::ImexProgress(_)
|
||||
| EventType::SecurejoinInviterProgress { .. }
|
||||
| EventType::SecurejoinJoinerProgress { .. }
|
||||
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
EventType::ImexFileWritten(file) => {
|
||||
let data2 = file.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_event_get_account_id(event: *mut dc_event_t) -> u32 {
|
||||
if event.is_null() {
|
||||
eprintln!("ignoring careless call to dc_event_get_account_id()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
(*event).id
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub type dc_event_emitter_t = EventEmitter;
|
||||
|
||||
@@ -1286,7 +1311,9 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration(
|
||||
let muteDuration = match duration {
|
||||
0 => MuteDuration::NotMuted,
|
||||
-1 => MuteDuration::Forever,
|
||||
n if n > 0 => MuteDuration::Until(SystemTime::now() + Duration::from_secs(duration as u64)),
|
||||
n if n > 0 => SystemTime::now()
|
||||
.checked_add(Duration::from_secs(duration as u64))
|
||||
.map_or(MuteDuration::Forever, MuteDuration::Until),
|
||||
_ => {
|
||||
warn!(
|
||||
ctx,
|
||||
@@ -2443,7 +2470,7 @@ pub unsafe extern "C" fn dc_chat_get_remaining_mute_duration(chat: *mut dc_chat_
|
||||
MuteDuration::NotMuted => 0,
|
||||
MuteDuration::Forever => -1,
|
||||
MuteDuration::Until(when) => when
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.duration_since(SystemTime::now())
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0),
|
||||
}
|
||||
@@ -3333,3 +3360,250 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
|
||||
// currently, there is nothing to free, the provider info is a static object.
|
||||
// this may change once we start localizing string.
|
||||
}
|
||||
|
||||
// -- Accounts
|
||||
|
||||
/// Struct representing a list of deltachat accounts.
|
||||
pub type dc_accounts_t = Accounts;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_new(
|
||||
os_name: *const libc::c_char,
|
||||
dbfile: *const libc::c_char,
|
||||
) -> *mut dc_accounts_t {
|
||||
setup_panic!();
|
||||
|
||||
if dbfile.is_null() {
|
||||
eprintln!("ignoring careless call to 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()));
|
||||
|
||||
match accs {
|
||||
Ok(accs) => Box::into_raw(Box::new(accs)),
|
||||
Err(err) => {
|
||||
eprintln!("failed to create accounts: {}", err);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Release the accounts structure.
|
||||
///
|
||||
/// This function releases the memory of the `dc_accounts_t` structure.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_unref(accounts: *mut dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_unref()");
|
||||
return;
|
||||
}
|
||||
let _ = Box::from_raw(accounts);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
id: u32,
|
||||
) -> *mut dc_context_t {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_account()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.get_account(id))
|
||||
.map(|ctx| Box::into_raw(Box::new(ctx)))
|
||||
.unwrap_or_else(std::ptr::null_mut)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
) -> *mut dc_context_t {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_selected_account()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let ctx = block_on(accounts.get_selected_account());
|
||||
Box::into_raw(Box::new(ctx))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_select_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
id: u32,
|
||||
) -> libc::c_int {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_select_account()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.select_account(id))
|
||||
.map(|_| 1)
|
||||
.unwrap_or_else(|_| 0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -> u32 {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_add_account()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
|
||||
block_on(accounts.add_account()).unwrap_or_else(|_| 0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_remove_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
id: u32,
|
||||
) -> libc::c_int {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_remove_account()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
|
||||
block_on(accounts.remove_account(id))
|
||||
.map(|_| 1)
|
||||
.unwrap_or_else(|_| 0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_migrate_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
dbfile: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if accounts.is_null() || dbfile.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_migrate_account()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let dbfile = to_string_lossy(dbfile);
|
||||
|
||||
block_on(accounts.migrate_account(async_std::path::PathBuf::from(dbfile)))
|
||||
.map(|_| 1)
|
||||
.unwrap_or_else(|_| 0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *mut dc_array_t {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_all()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let list = block_on(accounts.get_all());
|
||||
let array: dc_array_t = list.into();
|
||||
|
||||
Box::into_raw(Box::new(array))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_import_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
file: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if accounts.is_null() || file.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_import_account()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let file = to_string_lossy(file);
|
||||
block_on(accounts.import_account(async_std::path::PathBuf::from(file)))
|
||||
.map(|_| 1)
|
||||
.unwrap_or_else(|_| 0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_start_io()");
|
||||
return;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.start_io());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_stop_io()");
|
||||
return;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.stop_io());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t) {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_mabye_network()");
|
||||
return;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.maybe_network());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub type dc_accounts_event_emitter_t = deltachat::accounts::EventEmitter;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
accounts: *mut dc_accounts_t,
|
||||
) -> *mut dc_accounts_event_emitter_t {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_event_emitter()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let emitter = block_on(accounts.get_event_emitter());
|
||||
|
||||
Box::into_raw(Box::new(emitter))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_event_emitter_unref(
|
||||
emitter: *mut dc_accounts_event_emitter_t,
|
||||
) {
|
||||
if emitter.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_event_emitter_unref()");
|
||||
return;
|
||||
}
|
||||
let _ = Box::from_raw(emitter);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_next_event(
|
||||
emitter: *mut dc_accounts_event_emitter_t,
|
||||
) -> *mut dc_event_t {
|
||||
if emitter.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let emitter = &*emitter;
|
||||
|
||||
emitter
|
||||
.recv_sync()
|
||||
.map(|ev| Box::into_raw(Box::new(ev)))
|
||||
.unwrap_or_else(ptr::null_mut)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
extern crate dirs;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
@@ -17,7 +19,7 @@ use deltachat::message::{self, Message, MessageState, MsgId};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::sql;
|
||||
use deltachat::Event;
|
||||
use deltachat::EventType;
|
||||
use deltachat::{config, provider};
|
||||
|
||||
/// Reset database tables.
|
||||
@@ -86,7 +88,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("(8) Rest but server config reset.");
|
||||
}
|
||||
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
@@ -157,7 +159,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
||||
if read_cnt > 0 {
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
@@ -284,7 +286,9 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
|
||||
"addr unset"
|
||||
}
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &addr).await;
|
||||
let peerstate = Peerstate::from_addr(context, &addr)
|
||||
.await
|
||||
.expect("peerstate error");
|
||||
if peerstate.is_some() && contact_id != 1 as libc::c_uint {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
@@ -440,17 +444,21 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
has_backup(&context, blobdir).await?;
|
||||
}
|
||||
"export-backup" => {
|
||||
imex(&context, ImexMode::ExportBackup, Some(blobdir)).await?;
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(&context, ImexMode::ExportBackup, Some(&dir)).await?;
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
}
|
||||
"import-backup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
|
||||
imex(&context, ImexMode::ImportBackup, Some(arg1)).await?;
|
||||
}
|
||||
"export-keys" => {
|
||||
imex(&context, ImexMode::ExportSelfKeys, Some(blobdir)).await?;
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(&context, ImexMode::ExportSelfKeys, Some(&dir)).await?;
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
}
|
||||
"import-keys" => {
|
||||
imex(&context, ImexMode::ImportSelfKeys, Some(blobdir)).await?;
|
||||
imex(&context, ImexMode::ImportSelfKeys, Some(arg1)).await?;
|
||||
}
|
||||
"export-setup" => {
|
||||
let setup_code = create_setup_code(&context);
|
||||
@@ -1057,7 +1065,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
// "event" => {
|
||||
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
|
||||
// let event = arg1.parse()?;
|
||||
// let event = Event::from_u32(event).ok_or(format_err!("Event::from_u32({})", event))?;
|
||||
// let event = EventType::from_u32(event).ok_or(format_err!("EventType::from_u32({})", event))?;
|
||||
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
|
||||
// println!(
|
||||
// "Sending event {:?}({}), received value {}.",
|
||||
|
||||
@@ -19,7 +19,7 @@ use deltachat::config;
|
||||
use deltachat::context::*;
|
||||
use deltachat::oauth2::*;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::Event;
|
||||
use deltachat::EventType;
|
||||
use log::{error, info, warn};
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::config::OutputStreamType;
|
||||
@@ -34,35 +34,35 @@ mod cmdline;
|
||||
use self::cmdline::*;
|
||||
|
||||
/// Event Handler
|
||||
fn receive_event(event: Event) {
|
||||
fn receive_event(event: EventType) {
|
||||
let yellow = Color::Yellow.normal();
|
||||
match event {
|
||||
Event::Info(msg) => {
|
||||
EventType::Info(msg) => {
|
||||
/* do not show the event as this would fill the screen */
|
||||
info!("{}", msg);
|
||||
}
|
||||
Event::SmtpConnected(msg) => {
|
||||
EventType::SmtpConnected(msg) => {
|
||||
info!("[SMTP_CONNECTED] {}", msg);
|
||||
}
|
||||
Event::ImapConnected(msg) => {
|
||||
EventType::ImapConnected(msg) => {
|
||||
info!("[IMAP_CONNECTED] {}", msg);
|
||||
}
|
||||
Event::SmtpMessageSent(msg) => {
|
||||
EventType::SmtpMessageSent(msg) => {
|
||||
info!("[SMTP_MESSAGE_SENT] {}", msg);
|
||||
}
|
||||
Event::Warning(msg) => {
|
||||
EventType::Warning(msg) => {
|
||||
warn!("{}", msg);
|
||||
}
|
||||
Event::Error(msg) => {
|
||||
EventType::Error(msg) => {
|
||||
error!("{}", msg);
|
||||
}
|
||||
Event::ErrorNetwork(msg) => {
|
||||
EventType::ErrorNetwork(msg) => {
|
||||
error!("[NETWORK] msg={}", msg);
|
||||
}
|
||||
Event::ErrorSelfNotInGroup(msg) => {
|
||||
EventType::ErrorSelfNotInGroup(msg) => {
|
||||
error!("[SELF_NOT_IN_GROUP] {}", msg);
|
||||
}
|
||||
Event::MsgsChanged { chat_id, msg_id } => {
|
||||
EventType::MsgsChanged { chat_id, msg_id } => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!(
|
||||
@@ -71,34 +71,34 @@ fn receive_event(event: Event) {
|
||||
))
|
||||
);
|
||||
}
|
||||
Event::ContactsChanged(_) => {
|
||||
EventType::ContactsChanged(_) => {
|
||||
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
|
||||
}
|
||||
Event::LocationChanged(contact) => {
|
||||
EventType::LocationChanged(contact) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
|
||||
);
|
||||
}
|
||||
Event::ConfigureProgress(progress) => {
|
||||
EventType::ConfigureProgress(progress) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
|
||||
);
|
||||
}
|
||||
Event::ImexProgress(progress) => {
|
||||
EventType::ImexProgress(progress) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received IMEX_PROGRESS({} ‰)", progress))
|
||||
);
|
||||
}
|
||||
Event::ImexFileWritten(file) => {
|
||||
EventType::ImexFileWritten(file) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received IMEX_FILE_WRITTEN({})", file.display()))
|
||||
);
|
||||
}
|
||||
Event::ChatModified(chat) => {
|
||||
EventType::ChatModified(chat) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received CHAT_MODIFIED({})", chat))
|
||||
@@ -272,12 +272,12 @@ 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()).await?;
|
||||
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf(), 0).await?;
|
||||
|
||||
let events = context.get_event_emitter();
|
||||
async_std::task::spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
receive_event(event);
|
||||
receive_event(event.typ);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,20 +6,20 @@ use deltachat::config;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::message::Message;
|
||||
use deltachat::Event;
|
||||
use deltachat::EventType;
|
||||
|
||||
fn cb(event: Event) {
|
||||
fn cb(event: EventType) {
|
||||
match event {
|
||||
Event::ConfigureProgress(progress) => {
|
||||
EventType::ConfigureProgress(progress) => {
|
||||
log::info!("progress: {}", progress);
|
||||
}
|
||||
Event::Info(msg) => {
|
||||
EventType::Info(msg) => {
|
||||
log::info!("{}", msg);
|
||||
}
|
||||
Event::Warning(msg) => {
|
||||
EventType::Warning(msg) => {
|
||||
log::warn!("{}", msg);
|
||||
}
|
||||
Event::Error(msg) | Event::ErrorNetwork(msg) => {
|
||||
EventType::Error(msg) | EventType::ErrorNetwork(msg) => {
|
||||
log::error!("{}", msg);
|
||||
}
|
||||
event => {
|
||||
@@ -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())
|
||||
let ctx = Context::new("FakeOs".into(), dbfile.into(), 0)
|
||||
.await
|
||||
.expect("Failed to create context");
|
||||
let info = ctx.get_info().await;
|
||||
@@ -45,7 +45,7 @@ async fn main() {
|
||||
let events = ctx.get_event_emitter();
|
||||
let events_spawn = async_std::task::spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
cb(event);
|
||||
cb(event.typ);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
1.44.0
|
||||
------
|
||||
|
||||
- fix Chat.get_mute_duration()
|
||||
|
||||
1.40.1
|
||||
---------------
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ class Account(object):
|
||||
def create_contact(self, obj, name=None):
|
||||
""" create a (new) Contact or return an existing one.
|
||||
|
||||
Calling this method will always resulut in the same
|
||||
Calling this method will always result in the same
|
||||
underlying contact id. If there already is a Contact
|
||||
with that e-mail address, it is unblocked and its display
|
||||
`name` is updated if specified.
|
||||
|
||||
@@ -137,7 +137,7 @@ class Chat(object):
|
||||
:param duration:
|
||||
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
|
||||
"""
|
||||
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
|
||||
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
|
||||
|
||||
def get_ephemeral_timer(self):
|
||||
""" get ephemeral timer.
|
||||
|
||||
@@ -51,6 +51,10 @@ class Contact(object):
|
||||
""" Return True if the contact is blocked. """
|
||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||
|
||||
def set_blocked(self, block=True):
|
||||
""" Block or unblock a contact. """
|
||||
return lib.dc_block_contact(self.account._dc_context, self.id, block)
|
||||
|
||||
def is_verified(self):
|
||||
""" Return True if the contact is verified. """
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
|
||||
@@ -108,6 +108,15 @@ class DirectImap:
|
||||
|
||||
def get_all_messages(self):
|
||||
assert not self._idling
|
||||
|
||||
# Flush unsolicited responses. IMAPClient has problems
|
||||
# dealing with them: https://github.com/mjs/imapclient/issues/334
|
||||
# When this NOOP was introduced, next FETCH returned empty
|
||||
# result instead of a single message, even though IMAP server
|
||||
# can only return more untagged responses than required, not
|
||||
# less.
|
||||
self.conn.noop()
|
||||
|
||||
return self.conn.fetch(ALL, [FLAGS])
|
||||
|
||||
def get_unread_messages(self):
|
||||
|
||||
@@ -276,15 +276,23 @@ class TestOfflineChat:
|
||||
def test_mute(self, ac1):
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
assert not chat.is_muted()
|
||||
assert chat.get_mute_duration() == 0
|
||||
chat.mute()
|
||||
assert chat.is_muted()
|
||||
assert chat.get_mute_duration() == -1
|
||||
chat.unmute()
|
||||
assert not chat.is_muted()
|
||||
chat.mute(50)
|
||||
assert chat.is_muted()
|
||||
assert chat.get_mute_duration() <= 50
|
||||
with pytest.raises(ValueError):
|
||||
chat.mute(-51)
|
||||
|
||||
# Regression test, this caused Rust panic previously
|
||||
chat.mute(2**63 - 1)
|
||||
assert chat.is_muted()
|
||||
assert chat.get_mute_duration() == -1
|
||||
|
||||
def test_delete_and_send_fails(self, ac1, chat1):
|
||||
chat1.delete()
|
||||
ac1._evtracker.wait_next_messages_changed()
|
||||
@@ -978,7 +986,10 @@ class TestOnlineAccount:
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("sending multi-line non-unicode message from ac1 to ac2")
|
||||
text1 = "hello\nworld"
|
||||
text1 = (
|
||||
"hello\nworld\nthis is a very long message that should be"
|
||||
+ " wrapped using format=flowed and unwrapped on the receiver"
|
||||
)
|
||||
msg_out = chat.send_text(text1)
|
||||
assert not msg_out.is_encrypted()
|
||||
|
||||
@@ -1394,6 +1405,46 @@ class TestOnlineAccount:
|
||||
assert ev.action == "removed"
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
|
||||
def test_system_group_msg_from_blocked_user(self, acfactory, lp):
|
||||
"""
|
||||
Tests that a blocked user removes you from a group.
|
||||
The message has to be fetched even though the user is blocked
|
||||
to avoid inconsistent group state.
|
||||
Also tests blocking in general.
|
||||
"""
|
||||
lp.sec("Create a group chat with ac1 and ac2")
|
||||
(ac1, ac2) = acfactory.get_two_online_accounts()
|
||||
acfactory.introduce_each_other((ac1, ac2))
|
||||
chat_on_ac1 = ac1.create_group_chat("title", contacts=[ac2])
|
||||
chat_on_ac1.send_text("First group message")
|
||||
chat_on_ac2 = ac2._evtracker.wait_next_incoming_message().chat
|
||||
|
||||
lp.sec("ac1 blocks ac2")
|
||||
contact = ac1.create_contact(ac2)
|
||||
contact.set_blocked()
|
||||
assert contact.is_blocked()
|
||||
|
||||
lp.sec("ac2 sends a message to ac1 that does not arrive because it is blocked")
|
||||
ac2.create_chat(ac1).send_text("This will not arrive!")
|
||||
|
||||
lp.sec("ac2 sends a group message to ac1 that arrives")
|
||||
# Groups would be hardly usable otherwise: If you have blocked some
|
||||
# users, they write messages and you only see replies to them without context
|
||||
chat_on_ac2.send_text("This will arrive")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "This will arrive"
|
||||
message_texts = [m.text for m in chat_on_ac1.get_messages()]
|
||||
assert len(message_texts) == 2
|
||||
assert "First group message" in message_texts
|
||||
assert "This will arrive" in message_texts
|
||||
|
||||
lp.sec("ac2 removes ac1 from their group")
|
||||
assert ac1.get_self_contact() in chat_on_ac1.get_contacts()
|
||||
assert contact.is_blocked()
|
||||
chat_on_ac2.remove_contact(ac1)
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
|
||||
assert not ac1.get_self_contact() in chat_on_ac1.get_contacts()
|
||||
|
||||
def test_set_get_group_image(self, acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
@@ -1652,6 +1703,76 @@ class TestOnlineAccount:
|
||||
assert "Ephemeral timer: " not in system_message2.get_message_info()
|
||||
assert chat1.get_ephemeral_timer() == 0
|
||||
|
||||
def test_delete_multiple_messages(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: sending seven messages")
|
||||
texts = ["first", "second", "third", "fourth", "fifth", "sixth", "seventh"]
|
||||
for text in texts:
|
||||
chat12.send_text(text)
|
||||
|
||||
lp.sec("ac2: waiting for all messages on the other side")
|
||||
to_delete = []
|
||||
for text in texts:
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text in texts
|
||||
if text != "third":
|
||||
to_delete.append(msg)
|
||||
|
||||
lp.sec("ac2: deleting all messages except third")
|
||||
assert len(to_delete) == len(texts) - 1
|
||||
ac2.delete_messages(to_delete)
|
||||
for msg in to_delete:
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
|
||||
ac2._evtracker.get_info_contains("close/expunge succeeded")
|
||||
|
||||
lp.sec("imap2: test that only one message is left")
|
||||
imap2 = ac2.direct_imap
|
||||
|
||||
assert len(imap2.get_all_messages()) == 1
|
||||
|
||||
def test_name_changes(self, acfactory):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1.set_config("displayname", "Account 1")
|
||||
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
contact = None
|
||||
|
||||
def update_name():
|
||||
"""Send a message from ac1 to ac2 to update the name"""
|
||||
nonlocal contact
|
||||
chat12.send_text("Hello")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
contact = msg.get_sender_contact()
|
||||
return contact.name
|
||||
|
||||
assert update_name() == "Account 1"
|
||||
|
||||
ac1.set_config("displayname", "Account 1 revision 2")
|
||||
assert update_name() == "Account 1 revision 2"
|
||||
|
||||
# Explicitly rename contact on ac2 to "Renamed"
|
||||
ac2.create_contact(contact, name="Renamed")
|
||||
assert contact.name == "Renamed"
|
||||
|
||||
# ac1 also renames itself into "Renamed"
|
||||
assert update_name() == "Renamed"
|
||||
ac1.set_config("displayname", "Renamed")
|
||||
assert update_name() == "Renamed"
|
||||
|
||||
# Contact name was set to "Renamed" explicitly before,
|
||||
# so it should not be changed.
|
||||
ac1.set_config("displayname", "Renamed again")
|
||||
updated_name = update_name()
|
||||
if updated_name == "Renamed again":
|
||||
# Known bug, mark as XFAIL
|
||||
pytest.xfail("Contact was renamed after explicit rename")
|
||||
else:
|
||||
# No renames should happen after explicit rename
|
||||
assert updated_name == "Renamed"
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
def test_group_many_members_add_leave_remove(self, acfactory, lp):
|
||||
|
||||
533
src/accounts.rs
Normal file
@@ -0,0 +1,533 @@
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::task::{Context as TaskContext, Poll};
|
||||
|
||||
use async_std::fs;
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::sync::{Arc, RwLock};
|
||||
use uuid::Uuid;
|
||||
|
||||
use anyhow::{ensure, Context as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::Result;
|
||||
use crate::events::Event;
|
||||
|
||||
/// Account manager, that can handle multiple accounts in a single place.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Accounts {
|
||||
dir: PathBuf,
|
||||
config: Config,
|
||||
accounts: Arc<RwLock<HashMap<u32, Context>>>,
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
pub async fn new(os_name: String, dir: PathBuf) -> Result<Self> {
|
||||
if !dir.exists().await {
|
||||
Accounts::create(os_name, &dir).await?;
|
||||
}
|
||||
|
||||
Accounts::open(dir).await
|
||||
}
|
||||
|
||||
/// Creates a new default structure, including a default account.
|
||||
pub async fn create(os_name: String, dir: &PathBuf) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
|
||||
// create default account
|
||||
let config = Config::new(os_name.clone(), dir).await?;
|
||||
let account_config = config.new_account(dir).await?;
|
||||
|
||||
Context::new(os_name, account_config.dbfile().into(), account_config.id)
|
||||
.await
|
||||
.context("failed to create default account")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
|
||||
/// no account exists and no config exists.
|
||||
pub async fn open(dir: PathBuf) -> Result<Self> {
|
||||
ensure!(dir.exists().await, "directory does not exist");
|
||||
|
||||
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?;
|
||||
|
||||
Ok(Self {
|
||||
dir,
|
||||
config,
|
||||
accounts: Arc::new(RwLock::new(accounts)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get an account by its `id`:
|
||||
pub async fn get_account(&self, id: u32) -> Option<Context> {
|
||||
self.accounts.read().await.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Get the currently selected account.
|
||||
pub async fn get_selected_account(&self) -> Context {
|
||||
let id = self.config.get_selected_account().await;
|
||||
self.accounts
|
||||
.read()
|
||||
.await
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.expect("inconsistent state")
|
||||
}
|
||||
|
||||
/// Select the given account.
|
||||
pub async fn select_account(&self, id: u32) -> Result<()> {
|
||||
self.config.select_account(id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a new account.
|
||||
pub async fn add_account(&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?;
|
||||
self.accounts.write().await.insert(account_config.id, ctx);
|
||||
|
||||
Ok(account_config.id)
|
||||
}
|
||||
|
||||
/// Remove an account.
|
||||
pub async fn remove_account(&self, id: u32) -> Result<()> {
|
||||
let ctx = self.accounts.write().await.remove(&id);
|
||||
ensure!(ctx.is_some(), "no account with this id: {}", id);
|
||||
let ctx = ctx.unwrap();
|
||||
ctx.stop_io().await;
|
||||
drop(ctx);
|
||||
|
||||
if let Some(cfg) = self.config.get_account(id).await {
|
||||
fs::remove_dir_all(async_std::path::PathBuf::from(&cfg.dir))
|
||||
.await
|
||||
.context("failed to remove account data")?;
|
||||
}
|
||||
self.config.remove_account(id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Migrate an existing account into this structure.
|
||||
pub async fn migrate_account(&self, dbfile: PathBuf) -> Result<u32> {
|
||||
let blobdir = Context::derive_blobdir(&dbfile);
|
||||
|
||||
ensure!(
|
||||
dbfile.exists().await,
|
||||
"no database found: {}",
|
||||
dbfile.display()
|
||||
);
|
||||
ensure!(
|
||||
blobdir.exists().await,
|
||||
"no blobdir found: {}",
|
||||
blobdir.display()
|
||||
);
|
||||
|
||||
let old_id = self.config.get_selected_account().await;
|
||||
|
||||
// create new account
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
|
||||
let new_dbfile = account_config.dbfile().into();
|
||||
let new_blobdir = Context::derive_blobdir(&new_dbfile);
|
||||
|
||||
let res = {
|
||||
fs::create_dir_all(&account_config.dir).await?;
|
||||
fs::rename(&dbfile, &new_dbfile).await?;
|
||||
fs::rename(&blobdir, &new_blobdir).await?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
let ctx = Context::with_blobdir(
|
||||
self.config.os_name().await,
|
||||
new_dbfile,
|
||||
new_blobdir,
|
||||
account_config.id,
|
||||
)
|
||||
.await?;
|
||||
self.accounts.write().await.insert(account_config.id, ctx);
|
||||
Ok(account_config.id)
|
||||
}
|
||||
Err(err) => {
|
||||
// remove temp account
|
||||
fs::remove_dir_all(async_std::path::PathBuf::from(&account_config.dir))
|
||||
.await
|
||||
.context("failed to remove account data")?;
|
||||
|
||||
self.config.remove_account(account_config.id).await?;
|
||||
|
||||
// set selection back
|
||||
self.select_account(old_id).await?;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a list of all account ids.
|
||||
pub async fn get_all(&self) -> Vec<u32> {
|
||||
self.accounts.read().await.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Import a backup using a new account and selects it.
|
||||
pub async fn import_account(&self, file: PathBuf) -> Result<u32> {
|
||||
let old_id = self.config.get_selected_account().await;
|
||||
|
||||
let id = self.add_account().await?;
|
||||
let ctx = self.get_account(id).await.expect("just added");
|
||||
|
||||
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, Some(file)).await {
|
||||
Ok(_) => Ok(id),
|
||||
Err(err) => {
|
||||
// remove temp account
|
||||
self.remove_account(id).await?;
|
||||
// set selection back
|
||||
self.select_account(old_id).await?;
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_io(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
account.start_io().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop_io(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
account.stop_io().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn maybe_network(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
account.maybe_network().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Unified event emitter.
|
||||
pub async fn get_event_emitter(&self) -> EventEmitter {
|
||||
let emitters = self
|
||||
.accounts
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.map(|(id, a)| EmitterWrapper {
|
||||
id: *id,
|
||||
emitter: a.get_event_emitter(),
|
||||
done: AtomicBool::new(false),
|
||||
})
|
||||
.collect();
|
||||
|
||||
EventEmitter(emitters)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter {
|
||||
/// Blocking recv of an event. Return `None` if the `Sender` has been droped.
|
||||
pub fn recv_sync(&self) -> Option<Event> {
|
||||
async_std::task::block_on(self.recv())
|
||||
}
|
||||
|
||||
/// Async recv of an event. Return `None` if the `Sender` has been droped.
|
||||
pub async fn recv(&self) -> Option<Event> {
|
||||
futures::future::poll_fn(|cx| Pin::new(self).recv_poll(cx)).await
|
||||
}
|
||||
|
||||
fn recv_poll(self: Pin<&Self>, _cx: &mut TaskContext<'_>) -> Poll<Option<Event>> {
|
||||
for e in &*self.0 {
|
||||
if e.done.load(Ordering::Acquire) {
|
||||
// skip emitters that are already done
|
||||
continue;
|
||||
}
|
||||
|
||||
match e.emitter.try_recv() {
|
||||
Ok(event) => return Poll::Ready(Some(event)),
|
||||
Err(async_std::sync::TryRecvError::Disconnected) => {
|
||||
e.done.store(true, Ordering::Release);
|
||||
}
|
||||
Err(async_std::sync::TryRecvError::Empty) => {}
|
||||
}
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventEmitter(Vec<EmitterWrapper>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EmitterWrapper {
|
||||
id: u32,
|
||||
emitter: crate::events::EventEmitter,
|
||||
done: AtomicBool,
|
||||
}
|
||||
|
||||
pub const CONFIG_NAME: &str = "accounts.toml";
|
||||
pub const DB_NAME: &str = "dc.db";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
file: PathBuf,
|
||||
inner: Arc<RwLock<InnerConfig>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct InnerConfig {
|
||||
pub os_name: String,
|
||||
/// The currently selected account.
|
||||
pub selected_account: u32,
|
||||
pub next_id: u32,
|
||||
pub accounts: Vec<AccountConfig>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn new(os_name: String, dir: &PathBuf) -> Result<Self> {
|
||||
let cfg = Config {
|
||||
file: dir.join(CONFIG_NAME),
|
||||
inner: Arc::new(RwLock::new(InnerConfig {
|
||||
os_name,
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
})),
|
||||
};
|
||||
|
||||
cfg.sync().await?;
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub async fn os_name(&self) -> String {
|
||||
self.inner.read().await.os_name.clone()
|
||||
}
|
||||
|
||||
/// Sync the inmemory representation to disk.
|
||||
async fn sync(&self) -> Result<()> {
|
||||
fs::write(
|
||||
&self.file,
|
||||
toml::to_string_pretty(&*self.inner.read().await)?,
|
||||
)
|
||||
.await
|
||||
.context("failed to write config")
|
||||
}
|
||||
|
||||
/// Read a configuration from the given file into memory.
|
||||
pub async fn from_file(file: PathBuf) -> Result<Self> {
|
||||
let bytes = fs::read(&file).await.context("failed to read file")?;
|
||||
let inner: InnerConfig = toml::from_slice(&bytes).context("failed to parse config")?;
|
||||
|
||||
Ok(Config {
|
||||
file,
|
||||
inner: Arc::new(RwLock::new(inner)),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn load_accounts(&self) -> Result<HashMap<u32, Context>> {
|
||||
let cfg = &*self.inner.read().await;
|
||||
let mut accounts = HashMap::with_capacity(cfg.accounts.len());
|
||||
for account_config in &cfg.accounts {
|
||||
let ctx = Context::new(
|
||||
cfg.os_name.clone(),
|
||||
account_config.dbfile().into(),
|
||||
account_config.id,
|
||||
)
|
||||
.await?;
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
/// Create a new account in the given root directory.
|
||||
pub async fn new_account(&self, dir: &PathBuf) -> Result<AccountConfig> {
|
||||
let id = {
|
||||
let inner = &mut self.inner.write().await;
|
||||
let id = inner.next_id;
|
||||
let uuid = Uuid::new_v4();
|
||||
let target_dir = dir.join(uuid.to_simple_ref().to_string());
|
||||
|
||||
inner.accounts.push(AccountConfig {
|
||||
id,
|
||||
name: String::new(),
|
||||
dir: target_dir.into(),
|
||||
uuid,
|
||||
});
|
||||
inner.next_id += 1;
|
||||
id
|
||||
};
|
||||
|
||||
self.sync().await?;
|
||||
|
||||
self.select_account(id).await.expect("just added");
|
||||
let cfg = self.get_account(id).await.expect("just added");
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
/// Removes an existing acccount entirely.
|
||||
pub async fn remove_account(&self, id: u32) -> Result<()> {
|
||||
{
|
||||
let inner = &mut *self.inner.write().await;
|
||||
if let Some(idx) = inner.accounts.iter().position(|e| e.id == id) {
|
||||
// remove account from the configs
|
||||
inner.accounts.remove(idx);
|
||||
}
|
||||
if inner.selected_account == id {
|
||||
// reset selected account
|
||||
inner.selected_account = inner.accounts.get(0).map(|e| e.id).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
self.sync().await
|
||||
}
|
||||
|
||||
pub async fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
self.inner
|
||||
.read()
|
||||
.await
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub async fn get_selected_account(&self) -> u32 {
|
||||
self.inner.read().await.selected_account
|
||||
}
|
||||
|
||||
pub async fn select_account(&self, id: u32) -> Result<()> {
|
||||
{
|
||||
let inner = &mut *self.inner.write().await;
|
||||
ensure!(
|
||||
inner.accounts.iter().any(|e| e.id == id),
|
||||
"invalid account id: {}",
|
||||
id
|
||||
);
|
||||
|
||||
inner.selected_account = id;
|
||||
}
|
||||
|
||||
self.sync().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct AccountConfig {
|
||||
/// Unique id.
|
||||
pub id: u32,
|
||||
/// Display name
|
||||
pub name: String,
|
||||
/// Root directory for all data for this account.
|
||||
pub dir: std::path::PathBuf,
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
impl AccountConfig {
|
||||
/// Get the canoncial dbfile name for this configuration.
|
||||
pub fn dbfile(&self) -> std::path::PathBuf {
|
||||
self.dir.join(DB_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_account_new_open() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts1").into();
|
||||
|
||||
let accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let accounts2 = Accounts::open(p).await.unwrap();
|
||||
|
||||
assert_eq!(accounts1.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts1.config.get_selected_account().await, 1);
|
||||
|
||||
assert_eq!(accounts1.dir, accounts2.dir);
|
||||
assert_eq!(
|
||||
&*accounts1.config.inner.read().await,
|
||||
&*accounts2.config.inner.read().await,
|
||||
);
|
||||
assert_eq!(
|
||||
accounts1.accounts.read().await.len(),
|
||||
accounts2.accounts.read().await.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_account_new_add_remove() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 2);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
assert_eq!(accounts.accounts.read().await.len(), 2);
|
||||
|
||||
accounts.select_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
accounts.remove_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 2);
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_migrate_account() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
let extern_dbfile: PathBuf = dir.path().join("other").into();
|
||||
let ctx = Context::new("my_os".into(), extern_dbfile.clone(), 0)
|
||||
.await
|
||||
.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
drop(ctx);
|
||||
|
||||
accounts
|
||||
.migrate_account(extern_dbfile.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(accounts.accounts.read().await.len(), 2);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 2);
|
||||
|
||||
let ctx = accounts.get_selected_account().await;
|
||||
assert_eq!(
|
||||
"me@mail.com",
|
||||
ctx.get_config(crate::config::Config::Addr).await.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::error::Error;
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::message;
|
||||
|
||||
/// Represents a file in the blob directory.
|
||||
@@ -67,7 +67,7 @@ impl<'a> BlobObject<'a> {
|
||||
blobdir,
|
||||
name: format!("$BLOBDIR/{}", name),
|
||||
};
|
||||
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
|
||||
context.emit_event(EventType::NewBlobFile(blob.as_name().to_string()));
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ impl<'a> BlobObject<'a> {
|
||||
blobdir: context.get_blobdir(),
|
||||
name: format!("$BLOBDIR/{}", name),
|
||||
};
|
||||
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
|
||||
context.emit_event(EventType::NewBlobFile(blob.as_name().to_string()));
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
|
||||
127
src/chat.rs
@@ -17,7 +17,7 @@ use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::ephemeral::{delete_expired_messages, schedule_ephemeral_task, Timer as EphemeralTimer};
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::job::{self, Action};
|
||||
use crate::message::{self, InvalidMsgId, Message, MessageState, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -185,7 +185,7 @@ impl ChatId {
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
});
|
||||
@@ -242,7 +242,7 @@ impl ChatId {
|
||||
.execute("DELETE FROM chats WHERE id=?;", paramsv![self])
|
||||
.await?;
|
||||
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
});
|
||||
@@ -268,7 +268,7 @@ impl ChatId {
|
||||
};
|
||||
|
||||
if changed {
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: self,
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
@@ -375,6 +375,16 @@ impl ChatId {
|
||||
}
|
||||
|
||||
pub async fn get_fresh_msg_cnt(self, context: &Context) -> usize {
|
||||
// this function is typically used to show a badge counter beside _each_ chatlist item.
|
||||
// to make this as fast as possible, esp. on older devices, we added an combined index over the rows used for querying.
|
||||
// so if you alter the query here, you may want to alter the index over `(state, hidden, chat_id)` in `sql.rs`.
|
||||
//
|
||||
// the impact of the index is significant once the database grows:
|
||||
// - on an older android4 with 18k messages, query-time decreased from 110ms to 2ms
|
||||
// - on an mid-class moto-g or iphone7 with 50k messages, query-time decreased from 26ms or 6ms to 0-1ms
|
||||
// the times are average, no matter if there are fresh messages or not -
|
||||
// and have to be multiplied by the number of items shown at once on the chatlist,
|
||||
// so savings up to 2 seconds are possible on older devices - newer ones will feel "snappier" :)
|
||||
context
|
||||
.sql
|
||||
.query_get_value::<i32>(
|
||||
@@ -789,7 +799,7 @@ impl Chat {
|
||||
{
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorSelfNotInGroup("Cannot send message; self not in group.".into())
|
||||
EventType::ErrorSelfNotInGroup("Cannot send message; self not in group.".into())
|
||||
);
|
||||
bail!("Cannot set message; self not in group.");
|
||||
}
|
||||
@@ -1146,7 +1156,7 @@ pub async fn create_by_msg_id(context: &Context, msg_id: MsgId) -> Result<ChatId
|
||||
chat.id.unblock(context).await;
|
||||
|
||||
// Sending with 0s as data since multiple messages may have changed.
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
@@ -1188,7 +1198,7 @@ pub async fn create_by_contact_id(context: &Context, contact_id: u32) -> Result<
|
||||
}
|
||||
};
|
||||
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
@@ -1355,7 +1365,7 @@ pub async fn prepare_msg(
|
||||
|
||||
msg.state = MessageState::OutPreparing;
|
||||
let msg_id = prepare_msg_common(context, chat_id, msg).await?;
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
@@ -1408,7 +1418,9 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Er
|
||||
message::guess_msgtype_from_suffix(&blob.to_abs_path())
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
msg.param.set(Param::MimeType, better_mime);
|
||||
if !msg.param.exists(Param::MimeType) {
|
||||
msg.param.set(Param::MimeType, better_mime);
|
||||
}
|
||||
}
|
||||
} else if !msg.param.exists(Param::MimeType) {
|
||||
if let Some((_, mime)) = message::guess_msgtype_from_suffix(&blob.to_abs_path()) {
|
||||
@@ -1530,7 +1542,7 @@ pub async fn send_msg_sync(
|
||||
|
||||
match status {
|
||||
job::Status::Finished(Ok(_)) => {
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
@@ -1558,13 +1570,13 @@ async fn send_msg_inner(
|
||||
if let Some(send_job) = prepare_send_msg(context, chat_id, msg).await? {
|
||||
job::add(context, send_job).await;
|
||||
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
|
||||
if msg.param.exists(Param::SetLatitude) {
|
||||
context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
|
||||
context.emit_event(EventType::LocationChanged(Some(DC_CONTACT_ID_SELF)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1629,13 +1641,7 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
|
||||
bail!("webrtc_instance not set");
|
||||
};
|
||||
|
||||
let room = dc_create_id();
|
||||
|
||||
let instance = if instance.contains("$ROOM") {
|
||||
instance.replace("$ROOM", &room)
|
||||
} else {
|
||||
format!("{}{}", instance, room)
|
||||
};
|
||||
let instance = Message::create_webrtc_instance(&instance, &dc_create_id());
|
||||
|
||||
let mut msg = Message::new(Viewtype::VideochatInvitation);
|
||||
msg.param.set(Param::WebrtcRoom, &instance);
|
||||
@@ -1665,7 +1671,7 @@ pub async fn get_chat_msgs(
|
||||
// On desktop chatlist is always shown on the side,
|
||||
// and it is important to update the last message shown
|
||||
// there.
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
})
|
||||
@@ -1788,7 +1794,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
@@ -1820,7 +1826,7 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<(), Error> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
});
|
||||
@@ -1958,7 +1964,8 @@ pub async fn create_group_chat(
|
||||
verified: VerifiedStatus,
|
||||
chat_name: impl AsRef<str>,
|
||||
) -> Result<ChatId, Error> {
|
||||
ensure!(!chat_name.as_ref().is_empty(), "Invalid chat name");
|
||||
let chat_name = improve_single_line_input(chat_name);
|
||||
ensure!(!chat_name.is_empty(), "Invalid chat name");
|
||||
|
||||
let draft_txt = context
|
||||
.stock_string_repl_str(StockMessage::NewGroupDraft, &chat_name)
|
||||
@@ -1973,7 +1980,7 @@ pub async fn create_group_chat(
|
||||
} else {
|
||||
Chattype::Group
|
||||
},
|
||||
chat_name.as_ref().to_string(),
|
||||
chat_name,
|
||||
grpid,
|
||||
time(),
|
||||
],
|
||||
@@ -1991,7 +1998,7 @@ pub async fn create_group_chat(
|
||||
chat_id.set_draft_raw(context, &mut draft_msg).await;
|
||||
}
|
||||
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
});
|
||||
@@ -2091,7 +2098,9 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorSelfNotInGroup("Cannot add contact to group; self not in group.".into())
|
||||
EventType::ErrorSelfNotInGroup(
|
||||
"Cannot add contact to group; self not in group.".into()
|
||||
)
|
||||
);
|
||||
bail!("can not add contact because our account is not part of it");
|
||||
}
|
||||
@@ -2149,7 +2158,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
msg.param.set_int(Param::Arg2, from_handshake.into());
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
}
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -2311,7 +2320,7 @@ pub async fn set_muted(
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
} else {
|
||||
bail!("Failed to set mute duration, chat might not exist -");
|
||||
}
|
||||
@@ -2343,7 +2352,7 @@ pub async fn remove_contact_from_chat(
|
||||
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await {
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorSelfNotInGroup(
|
||||
EventType::ErrorSelfNotInGroup(
|
||||
"Cannot remove contact from chat; self not in group.".into()
|
||||
)
|
||||
);
|
||||
@@ -2391,7 +2400,7 @@ pub async fn remove_contact_from_chat(
|
||||
// removed it first, it would complicate the
|
||||
// check/encryption logic.
|
||||
success = remove_from_chat_contacts_table(context, chat_id, contact_id).await;
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2436,22 +2445,23 @@ pub async fn set_chat_name(
|
||||
chat_id: ChatId,
|
||||
new_name: impl AsRef<str>,
|
||||
) -> Result<(), Error> {
|
||||
let new_name = improve_single_line_input(new_name);
|
||||
/* the function only sets the names of group chats; normal chats get their names from the contacts */
|
||||
let mut success = false;
|
||||
|
||||
ensure!(!new_name.as_ref().is_empty(), "Invalid name");
|
||||
ensure!(!new_name.is_empty(), "Invalid name");
|
||||
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
let mut msg = Message::default();
|
||||
|
||||
if real_group_exists(context, chat_id).await {
|
||||
if chat.name == new_name.as_ref() {
|
||||
if chat.name == new_name {
|
||||
success = true;
|
||||
} else if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await {
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorSelfNotInGroup("Cannot set chat name; self not in group".into())
|
||||
EventType::ErrorSelfNotInGroup("Cannot set chat name; self not in group".into())
|
||||
);
|
||||
} else {
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
@@ -2459,7 +2469,7 @@ pub async fn set_chat_name(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=?;",
|
||||
paramsv![new_name.as_ref().to_string(), chat_id],
|
||||
paramsv![new_name.to_string(), chat_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -2471,7 +2481,7 @@ pub async fn set_chat_name(
|
||||
.stock_system_msg(
|
||||
StockMessage::MsgGrpName,
|
||||
&chat.name,
|
||||
new_name.as_ref(),
|
||||
&new_name,
|
||||
DC_CONTACT_ID_SELF,
|
||||
)
|
||||
.await,
|
||||
@@ -2481,12 +2491,12 @@ pub async fn set_chat_name(
|
||||
msg.param.set(Param::Arg, &chat.name);
|
||||
}
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
}
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
@@ -2519,7 +2529,9 @@ pub async fn set_chat_profile_image(
|
||||
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await {
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorSelfNotInGroup("Cannot set chat profile image; self not in group.".into())
|
||||
EventType::ErrorSelfNotInGroup(
|
||||
"Cannot set chat profile image; self not in group.".into()
|
||||
)
|
||||
);
|
||||
bail!("Failed to set profile image");
|
||||
}
|
||||
@@ -2558,13 +2570,13 @@ pub async fn set_chat_profile_image(
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
emit_event!(
|
||||
context,
|
||||
Event::MsgsChanged {
|
||||
EventType::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: msg.id
|
||||
}
|
||||
);
|
||||
}
|
||||
emit_event!(context, Event::ChatModified(chat_id));
|
||||
emit_event!(context, EventType::ChatModified(chat_id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2648,7 +2660,7 @@ pub async fn forward_msgs(
|
||||
}
|
||||
}
|
||||
for (chat_id, msg_id) in created_chats.iter().zip(created_msgs.iter()) {
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: *chat_id,
|
||||
msg_id: *msg_id,
|
||||
});
|
||||
@@ -2774,9 +2786,9 @@ pub async fn add_device_msg_with_importance(
|
||||
|
||||
if !msg_id.is_unset() {
|
||||
if important {
|
||||
context.emit_event(Event::IncomingMsg { chat_id, msg_id });
|
||||
context.emit_event(EventType::IncomingMsg { chat_id, msg_id });
|
||||
} else {
|
||||
context.emit_event(Event::MsgsChanged { chat_id, msg_id });
|
||||
context.emit_event(EventType::MsgsChanged { chat_id, msg_id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2864,7 +2876,7 @@ pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl
|
||||
.get_rowid(context, "msgs", "rfc724_mid", &rfc724_mid)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: MsgId::new(row_id),
|
||||
});
|
||||
@@ -2874,6 +2886,7 @@ pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat;
|
||||
use crate::contact::Contact;
|
||||
use crate::test_utils::*;
|
||||
|
||||
@@ -3536,4 +3549,30 @@ mod tests {
|
||||
chat_id.set_draft(&t.ctx, Some(&mut msg)).await;
|
||||
assert!(!chat_id.parent_is_encrypted(&t.ctx).await.unwrap());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_fresh_msg_cnt() {
|
||||
let t = TestContext::new().await;
|
||||
// create 50 chats with 1k messages each
|
||||
let mut chat_ids = Vec::new();
|
||||
for _i in 1..50 {
|
||||
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
for _i in 1..1000 {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("message text".to_string());
|
||||
chat::send_msg(&t.ctx, chat_id, &mut msg)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
}
|
||||
chat_ids.push(chat_id);
|
||||
}
|
||||
// load all chats 20k times
|
||||
for _i in 1..20_000 {
|
||||
for chat_id in chat_ids.iter() {
|
||||
chat_id.get_fresh_msg_cnt(&t.ctx).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ use crate::chat::ChatId;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::job;
|
||||
use crate::message::MsgId;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::stock::StockMessage;
|
||||
@@ -24,11 +25,13 @@ pub enum Config {
|
||||
MailUser,
|
||||
MailPw,
|
||||
MailPort,
|
||||
MailSecurity,
|
||||
ImapCertificateChecks,
|
||||
SendServer,
|
||||
SendUser,
|
||||
SendPw,
|
||||
SendPort,
|
||||
SendSecurity,
|
||||
SmtpCertificateChecks,
|
||||
ServerFlags,
|
||||
|
||||
@@ -226,12 +229,21 @@ impl Context {
|
||||
Config::DeleteDeviceAfter => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
// Force chatlist reload to delete old messages immediately.
|
||||
self.emit_event(Event::MsgsChanged {
|
||||
self.emit_event(EventType::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
});
|
||||
ret
|
||||
}
|
||||
Config::Displayname => {
|
||||
let value = value.map(improve_single_line_input);
|
||||
self.sql.set_raw_config(self, key, value.as_deref()).await
|
||||
}
|
||||
Config::DeleteServerAfter => {
|
||||
let ret = self.sql.set_raw_config(self, key, value).await;
|
||||
job::schedule_resync(self).await;
|
||||
ret
|
||||
}
|
||||
_ => self.sql.set_raw_config(self, key, value).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
//! # Thunderbird's Autoconfiguration implementation
|
||||
//!
|
||||
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration
|
||||
use quick_xml::events::{BytesStart, Event};
|
||||
|
||||
use std::io::BufRead;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::Error;
|
||||
use super::{Error, ServerParams};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MozAutoconfigure<'a> {
|
||||
pub in_emailaddr: &'a str,
|
||||
pub in_emaildomain: &'a str,
|
||||
pub in_emaillocalpart: &'a str,
|
||||
pub out: LoginParam,
|
||||
pub out_imap_set: bool,
|
||||
pub out_smtp_set: bool,
|
||||
pub tag_server: MozServer,
|
||||
pub tag_config: MozConfigTag,
|
||||
struct Server {
|
||||
pub typ: String,
|
||||
pub hostname: String,
|
||||
pub port: u16,
|
||||
pub sockettype: Socket,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum MozServer {
|
||||
Undefined,
|
||||
Imap,
|
||||
Smtp,
|
||||
#[derive(Debug)]
|
||||
struct MozAutoconfigure {
|
||||
pub incoming_servers: Vec<Server>,
|
||||
pub outgoing_servers: Vec<Server>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -38,10 +37,147 @@ enum MozConfigTag {
|
||||
Username,
|
||||
}
|
||||
|
||||
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
|
||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
||||
reader.trim_text(true);
|
||||
impl Default for MozConfigTag {
|
||||
fn default() -> Self {
|
||||
Self::Undefined
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MozConfigTag {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.trim().to_lowercase().as_ref() {
|
||||
"hostname" => Ok(MozConfigTag::Hostname),
|
||||
"port" => Ok(MozConfigTag::Port),
|
||||
"sockettype" => Ok(MozConfigTag::Sockettype),
|
||||
"username" => Ok(MozConfigTag::Username),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a single IncomingServer or OutgoingServer section.
|
||||
fn parse_server<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
server_event: &BytesStart,
|
||||
) -> Result<Option<Server>, quick_xml::Error> {
|
||||
let end_tag = String::from_utf8_lossy(server_event.name())
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
|
||||
let typ = server_event
|
||||
.attributes()
|
||||
.find(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type")
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.map(|typ| {
|
||||
typ.unwrap()
|
||||
.unescape_and_decode_value(reader)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut hostname = None;
|
||||
let mut port = None;
|
||||
let mut sockettype = Socket::Automatic;
|
||||
let mut username = None;
|
||||
|
||||
let mut tag_config = MozConfigTag::Undefined;
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
match reader.read_event(&mut buf)? {
|
||||
Event::Start(ref event) => {
|
||||
tag_config = String::from_utf8_lossy(event.name())
|
||||
.parse()
|
||||
.unwrap_or_default();
|
||||
}
|
||||
Event::End(ref event) => {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
if tag == end_tag {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Text(ref event) => {
|
||||
let val = event
|
||||
.unescape_and_decode(reader)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
match tag_config {
|
||||
MozConfigTag::Hostname => hostname = Some(val),
|
||||
MozConfigTag::Port => port = Some(val.parse().unwrap_or_default()),
|
||||
MozConfigTag::Username => username = Some(val),
|
||||
MozConfigTag::Sockettype => {
|
||||
sockettype = match val.to_lowercase().as_ref() {
|
||||
"ssl" => Socket::SSL,
|
||||
"starttls" => Socket::STARTTLS,
|
||||
"plain" => Socket::Plain,
|
||||
_ => Socket::Automatic,
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Event::Eof => break,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(hostname), Some(port), Some(username)) = (hostname, port, username) {
|
||||
Ok(Some(Server {
|
||||
typ,
|
||||
hostname,
|
||||
port,
|
||||
sockettype,
|
||||
username,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_xml_reader<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
) -> Result<MozAutoconfigure, quick_xml::Error> {
|
||||
let mut incoming_servers = Vec::new();
|
||||
let mut outgoing_servers = Vec::new();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
match reader.read_event(&mut buf)? {
|
||||
Event::Start(ref event) => {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
if tag == "incomingserver" {
|
||||
if let Some(incoming_server) = parse_server(reader, event)? {
|
||||
incoming_servers.push(incoming_server);
|
||||
}
|
||||
} else if tag == "outgoingserver" {
|
||||
if let Some(outgoing_server) = parse_server(reader, event)? {
|
||||
outgoing_servers.push(outgoing_server);
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Eof => break,
|
||||
_ => (),
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
Ok(MozAutoconfigure {
|
||||
incoming_servers,
|
||||
outgoing_servers,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses XML and fills in address and domain placeholders.
|
||||
fn parse_xml_with_address(in_emailaddr: &str, xml_raw: &str) -> Result<MozAutoconfigure, Error> {
|
||||
// Split address into local part and domain part.
|
||||
let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect();
|
||||
let (in_emaillocalpart, in_emaildomain) = match &parts[..] {
|
||||
@@ -49,59 +185,78 @@ fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
|
||||
_ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())),
|
||||
};
|
||||
|
||||
let mut moz_ac = MozAutoconfigure {
|
||||
in_emailaddr,
|
||||
in_emaildomain,
|
||||
in_emaillocalpart,
|
||||
out: LoginParam::new(),
|
||||
out_imap_set: false,
|
||||
out_smtp_set: false,
|
||||
tag_server: MozServer::Undefined,
|
||||
tag_config: MozConfigTag::Undefined,
|
||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
||||
reader.trim_text(true);
|
||||
|
||||
let moz_ac = parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
|
||||
position: reader.buffer_position(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
let fill_placeholders = |val: &str| -> String {
|
||||
val.replace("%EMAILADDRESS%", in_emailaddr)
|
||||
.replace("%EMAILLOCALPART%", in_emaillocalpart)
|
||||
.replace("%EMAILDOMAIN%", in_emaildomain)
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
let event = reader
|
||||
.read_event(&mut buf)
|
||||
.map_err(|error| Error::InvalidXml {
|
||||
position: reader.buffer_position(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
match event {
|
||||
quick_xml::events::Event::Start(ref e) => {
|
||||
moz_autoconfigure_starttag_cb(e, &mut moz_ac, &reader)
|
||||
}
|
||||
quick_xml::events::Event::End(ref e) => moz_autoconfigure_endtag_cb(e, &mut moz_ac),
|
||||
quick_xml::events::Event::Text(ref e) => {
|
||||
moz_autoconfigure_text_cb(e, &mut moz_ac, &reader)
|
||||
}
|
||||
quick_xml::events::Event::Eof => break,
|
||||
_ => (),
|
||||
let fill_server_placeholders = |server: Server| -> Server {
|
||||
Server {
|
||||
typ: server.typ,
|
||||
hostname: fill_placeholders(&server.hostname),
|
||||
port: server.port,
|
||||
sockettype: server.sockettype,
|
||||
username: fill_placeholders(&server.username),
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
};
|
||||
|
||||
if moz_ac.out.mail_server.is_empty()
|
||||
|| moz_ac.out.mail_port == 0
|
||||
|| moz_ac.out.send_server.is_empty()
|
||||
|| moz_ac.out.send_port == 0
|
||||
{
|
||||
Err(Error::IncompleteAutoconfig(moz_ac.out))
|
||||
} else {
|
||||
Ok(moz_ac.out)
|
||||
}
|
||||
Ok(MozAutoconfigure {
|
||||
incoming_servers: moz_ac
|
||||
.incoming_servers
|
||||
.into_iter()
|
||||
.map(fill_server_placeholders)
|
||||
.collect(),
|
||||
outgoing_servers: moz_ac
|
||||
.outgoing_servers
|
||||
.into_iter()
|
||||
.map(fill_server_placeholders)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn moz_autoconfigure(
|
||||
/// Parses XML into `ServerParams` vector.
|
||||
fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerParams>, Error> {
|
||||
let moz_ac = parse_xml_with_address(in_emailaddr, xml_raw)?;
|
||||
|
||||
let res = moz_ac
|
||||
.incoming_servers
|
||||
.into_iter()
|
||||
.chain(moz_ac.outgoing_servers.into_iter())
|
||||
.filter_map(|server| {
|
||||
let protocol = match server.typ.as_ref() {
|
||||
"imap" => Some(Protocol::IMAP),
|
||||
"smtp" => Some(Protocol::SMTP),
|
||||
_ => None,
|
||||
};
|
||||
Some(ServerParams {
|
||||
protocol: protocol?,
|
||||
socket: server.sockettype,
|
||||
hostname: server.hostname,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub(crate) async fn moz_autoconfigure(
|
||||
context: &Context,
|
||||
url: &str,
|
||||
param_in: &LoginParam,
|
||||
) -> Result<LoginParam, Error> {
|
||||
) -> Result<Vec<ServerParams>, Error> {
|
||||
let xml_raw = read_url(context, url).await?;
|
||||
|
||||
let res = parse_xml(¶m_in.addr, &xml_raw);
|
||||
let res = parse_serverparams(¶m_in.addr, &xml_raw);
|
||||
if let Err(err) = &res {
|
||||
warn!(
|
||||
context,
|
||||
@@ -111,212 +266,60 @@ pub async fn moz_autoconfigure(
|
||||
res
|
||||
}
|
||||
|
||||
fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
|
||||
event: &BytesText,
|
||||
moz_ac: &mut MozAutoconfigure,
|
||||
reader: &quick_xml::Reader<B>,
|
||||
) {
|
||||
let val = event.unescape_and_decode(reader).unwrap_or_default();
|
||||
|
||||
let addr = moz_ac.in_emailaddr;
|
||||
let email_local = moz_ac.in_emaillocalpart;
|
||||
let email_domain = moz_ac.in_emaildomain;
|
||||
|
||||
let val = val
|
||||
.trim()
|
||||
.replace("%EMAILADDRESS%", addr)
|
||||
.replace("%EMAILLOCALPART%", email_local)
|
||||
.replace("%EMAILDOMAIN%", email_domain);
|
||||
|
||||
match moz_ac.tag_server {
|
||||
MozServer::Imap => match moz_ac.tag_config {
|
||||
MozConfigTag::Hostname => moz_ac.out.mail_server = val,
|
||||
MozConfigTag::Port => moz_ac.out.mail_port = val.parse().unwrap_or_default(),
|
||||
MozConfigTag::Username => moz_ac.out.mail_user = val,
|
||||
MozConfigTag::Sockettype => {
|
||||
let val_lower = val.to_lowercase();
|
||||
if val_lower == "ssl" {
|
||||
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
|
||||
}
|
||||
if val_lower == "starttls" {
|
||||
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS as i32
|
||||
}
|
||||
if val_lower == "plain" {
|
||||
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
MozServer::Smtp => match moz_ac.tag_config {
|
||||
MozConfigTag::Hostname => moz_ac.out.send_server = val,
|
||||
MozConfigTag::Port => moz_ac.out.send_port = val.parse().unwrap_or_default(),
|
||||
MozConfigTag::Username => moz_ac.out.send_user = val,
|
||||
MozConfigTag::Sockettype => {
|
||||
let val_lower = val.to_lowercase();
|
||||
if val_lower == "ssl" {
|
||||
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
|
||||
}
|
||||
if val_lower == "starttls" {
|
||||
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32
|
||||
}
|
||||
if val_lower == "plain" {
|
||||
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
MozServer::Undefined => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn moz_autoconfigure_endtag_cb(event: &BytesEnd, moz_ac: &mut MozAutoconfigure) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
if tag == "incomingserver" {
|
||||
if moz_ac.tag_server == MozServer::Imap {
|
||||
moz_ac.out_imap_set = true;
|
||||
}
|
||||
moz_ac.tag_server = MozServer::Undefined;
|
||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
||||
} else if tag == "outgoingserver" {
|
||||
if moz_ac.tag_server == MozServer::Smtp {
|
||||
moz_ac.out_smtp_set = true;
|
||||
}
|
||||
moz_ac.tag_server = MozServer::Undefined;
|
||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
||||
} else {
|
||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
fn moz_autoconfigure_starttag_cb<B: std::io::BufRead>(
|
||||
event: &BytesStart,
|
||||
moz_ac: &mut MozAutoconfigure,
|
||||
reader: &quick_xml::Reader<B>,
|
||||
) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
if tag == "incomingserver" {
|
||||
moz_ac.tag_server = if let Some(typ) = event.attributes().find(|attr| {
|
||||
attr.as_ref()
|
||||
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type")
|
||||
.unwrap_or_default()
|
||||
}) {
|
||||
let typ = typ
|
||||
.unwrap()
|
||||
.unescape_and_decode_value(reader)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
|
||||
if typ == "imap" && !moz_ac.out_imap_set {
|
||||
MozServer::Imap
|
||||
} else {
|
||||
MozServer::Undefined
|
||||
}
|
||||
} else {
|
||||
MozServer::Undefined
|
||||
};
|
||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
||||
} else if tag == "outgoingserver" {
|
||||
moz_ac.tag_server = if !moz_ac.out_smtp_set {
|
||||
MozServer::Smtp
|
||||
} else {
|
||||
MozServer::Undefined
|
||||
};
|
||||
moz_ac.tag_config = MozConfigTag::Undefined;
|
||||
} else if tag == "hostname" {
|
||||
moz_ac.tag_config = MozConfigTag::Hostname;
|
||||
} else if tag == "port" {
|
||||
moz_ac.tag_config = MozConfigTag::Port;
|
||||
} else if tag == "sockettype" {
|
||||
moz_ac.tag_config = MozConfigTag::Sockettype;
|
||||
} else if tag == "username" {
|
||||
moz_ac.tag_config = MozConfigTag::Username;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_outlook_autoconfig() {
|
||||
// Copied from https://autoconfig.thunderbird.net/v1.1/outlook.com on 2019-10-11
|
||||
let xml_raw =
|
||||
"<clientConfig version=\"1.1\">
|
||||
<emailProvider id=\"outlook.com\">
|
||||
<domain>hotmail.com</domain>
|
||||
<domain>hotmail.co.uk</domain>
|
||||
<domain>hotmail.co.jp</domain>
|
||||
<domain>hotmail.com.br</domain>
|
||||
<domain>hotmail.de</domain>
|
||||
<domain>hotmail.fr</domain>
|
||||
<domain>hotmail.it</domain>
|
||||
<domain>hotmail.es</domain>
|
||||
<domain>live.com</domain>
|
||||
<domain>live.co.uk</domain>
|
||||
<domain>live.co.jp</domain>
|
||||
<domain>live.de</domain>
|
||||
<domain>live.fr</domain>
|
||||
<domain>live.it</domain>
|
||||
<domain>live.jp</domain>
|
||||
<domain>msn.com</domain>
|
||||
<domain>outlook.com</domain>
|
||||
<displayName>Outlook.com (Microsoft)</displayName>
|
||||
<displayShortName>Outlook</displayShortName>
|
||||
<incomingServer type=\"exchange\">
|
||||
<hostname>outlook.office365.com</hostname>
|
||||
<port>443</port>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>OAuth2</authentication>
|
||||
<owaURL>https://outlook.office365.com/owa/</owaURL>
|
||||
<ewsURL>https://outlook.office365.com/ews/exchange.asmx</ewsURL>
|
||||
<useGlobalPreferredServer>true</useGlobalPreferredServer>
|
||||
</incomingServer>
|
||||
<incomingServer type=\"imap\">
|
||||
<hostname>outlook.office365.com</hostname>
|
||||
<port>993</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
<incomingServer type=\"pop3\">
|
||||
<hostname>outlook.office365.com</hostname>
|
||||
<port>995</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<pop3>
|
||||
<leaveMessagesOnServer>true</leaveMessagesOnServer>
|
||||
<!-- Outlook.com docs specifically mention that POP3 deletes have effect on the main inbox on webmail and IMAP -->
|
||||
</pop3>
|
||||
</incomingServer>
|
||||
<outgoingServer type=\"smtp\">
|
||||
<hostname>smtp.office365.com</hostname>
|
||||
<port>587</port>
|
||||
<socketType>STARTTLS</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</outgoingServer>
|
||||
<documentation url=\"http://windows.microsoft.com/en-US/windows/outlook/send-receive-from-app\">
|
||||
<descr lang=\"en\">Set up an email app with Outlook.com</descr>
|
||||
</documentation>
|
||||
</emailProvider>
|
||||
<webMail>
|
||||
<loginPage url=\"https://www.outlook.com/\"/>
|
||||
<loginPageInfo url=\"https://www.outlook.com/\">
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<usernameField id=\"i0116\" name=\"login\"/>
|
||||
<passwordField id=\"i0118\" name=\"passwd\"/>
|
||||
<loginButton id=\"idSIButton9\" name=\"SI\"/>
|
||||
</loginPageInfo>
|
||||
</webMail>
|
||||
</clientConfig>";
|
||||
let res = parse_xml("example@outlook.com", xml_raw).expect("XML parsing failed");
|
||||
assert_eq!(res.mail_server, "outlook.office365.com");
|
||||
assert_eq!(res.mail_port, 993);
|
||||
assert_eq!(res.send_server, "smtp.office365.com");
|
||||
assert_eq!(res.send_port, 587);
|
||||
let xml_raw = include_str!("../../test-data/autoconfig/outlook.com.xml");
|
||||
let res = parse_serverparams("example@outlook.com", xml_raw).expect("XML parsing failed");
|
||||
assert_eq!(res[0].protocol, Protocol::IMAP);
|
||||
assert_eq!(res[0].hostname, "outlook.office365.com");
|
||||
assert_eq!(res[0].port, 993);
|
||||
assert_eq!(res[1].protocol, Protocol::SMTP);
|
||||
assert_eq!(res[1].hostname, "smtp.office365.com");
|
||||
assert_eq!(res[1].port, 587);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_lakenet_autoconfig() {
|
||||
let xml_raw = include_str!("../../test-data/autoconfig/lakenet.ch.xml");
|
||||
let res =
|
||||
parse_xml_with_address("example@lakenet.ch", xml_raw).expect("XML parsing failed");
|
||||
|
||||
assert_eq!(res.incoming_servers.len(), 4);
|
||||
|
||||
assert_eq!(res.incoming_servers[0].typ, "imap");
|
||||
assert_eq!(res.incoming_servers[0].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[0].port, 993);
|
||||
assert_eq!(res.incoming_servers[0].sockettype, Socket::SSL);
|
||||
assert_eq!(res.incoming_servers[0].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.incoming_servers[1].typ, "imap");
|
||||
assert_eq!(res.incoming_servers[1].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[1].port, 143);
|
||||
assert_eq!(res.incoming_servers[1].sockettype, Socket::STARTTLS);
|
||||
assert_eq!(res.incoming_servers[1].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.incoming_servers[2].typ, "pop3");
|
||||
assert_eq!(res.incoming_servers[2].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[2].port, 995);
|
||||
assert_eq!(res.incoming_servers[2].sockettype, Socket::SSL);
|
||||
assert_eq!(res.incoming_servers[2].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.incoming_servers[3].typ, "pop3");
|
||||
assert_eq!(res.incoming_servers[3].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.incoming_servers[3].port, 110);
|
||||
assert_eq!(res.incoming_servers[3].sockettype, Socket::STARTTLS);
|
||||
assert_eq!(res.incoming_servers[3].username, "example@lakenet.ch");
|
||||
|
||||
assert_eq!(res.outgoing_servers.len(), 1);
|
||||
|
||||
assert_eq!(res.outgoing_servers[0].typ, "smtp");
|
||||
assert_eq!(res.outgoing_servers[0].hostname, "mail.lakenet.ch");
|
||||
assert_eq!(res.outgoing_servers[0].port, 587);
|
||||
assert_eq!(res.outgoing_servers[0].sockettype, Socket::STARTTLS);
|
||||
assert_eq!(res.outgoing_servers[0].username, "example@lakenet.ch");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +1,194 @@
|
||||
//! Outlook's Autodiscover
|
||||
//! # Outlook's Autodiscover
|
||||
//!
|
||||
//! This module implements autoconfiguration via POX (Plain Old XML) interface to Autodiscover
|
||||
//! Service. Newer SOAP interface, introduced in Exchange 2010, is not used.
|
||||
|
||||
use quick_xml::events::BytesEnd;
|
||||
use quick_xml::events::Event;
|
||||
|
||||
use std::io::BufRead;
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::Error;
|
||||
use super::{Error, ServerParams};
|
||||
|
||||
struct OutlookAutodiscover {
|
||||
pub out: LoginParam,
|
||||
pub out_imap_set: bool,
|
||||
pub out_smtp_set: bool,
|
||||
pub config_type: Option<String>,
|
||||
pub config_server: String,
|
||||
pub config_port: i32,
|
||||
pub config_ssl: String,
|
||||
pub config_redirecturl: Option<String>,
|
||||
/// Result of parsing a single `Protocol` tag.
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox
|
||||
#[derive(Debug)]
|
||||
struct ProtocolTag {
|
||||
/// Server type, such as "IMAP", "SMTP" or "POP3".
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox
|
||||
pub typ: String,
|
||||
|
||||
/// Server identifier, hostname or IP address for IMAP and SMTP.
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox
|
||||
pub server: String,
|
||||
|
||||
/// Network port.
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox
|
||||
pub port: u16,
|
||||
|
||||
/// Whether connection should be secure, "on" or "off", default is "on".
|
||||
///
|
||||
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox
|
||||
pub ssl: bool,
|
||||
}
|
||||
|
||||
enum ParsingResult {
|
||||
LoginParam(LoginParam),
|
||||
Protocols(Vec<ProtocolTag>),
|
||||
|
||||
/// XML redirect via `RedirectUrl` tag.
|
||||
RedirectUrl(String),
|
||||
}
|
||||
|
||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
|
||||
let mut outlk_ad = OutlookAutodiscover {
|
||||
out: LoginParam::new(),
|
||||
out_imap_set: false,
|
||||
out_smtp_set: false,
|
||||
config_type: None,
|
||||
config_server: String::new(),
|
||||
config_port: 0,
|
||||
config_ssl: String::new(),
|
||||
config_redirecturl: None,
|
||||
};
|
||||
|
||||
let mut reader = quick_xml::Reader::from_str(&xml_raw);
|
||||
reader.trim_text(true);
|
||||
/// Parses a single Protocol section.
|
||||
fn parse_protocol<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
) -> Result<Option<ProtocolTag>, quick_xml::Error> {
|
||||
let mut protocol_type = None;
|
||||
let mut protocol_server = None;
|
||||
let mut protocol_port = None;
|
||||
let mut protocol_ssl = true;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let mut current_tag: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let event = reader
|
||||
.read_event(&mut buf)
|
||||
.map_err(|error| Error::InvalidXml {
|
||||
position: reader.buffer_position(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
match event {
|
||||
quick_xml::events::Event::Start(ref e) => {
|
||||
let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase();
|
||||
|
||||
match reader.read_event(&mut buf)? {
|
||||
Event::Start(ref event) => {
|
||||
current_tag = Some(String::from_utf8_lossy(event.name()).trim().to_lowercase());
|
||||
}
|
||||
Event::End(ref event) => {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
if tag == "protocol" {
|
||||
outlk_ad.config_type = None;
|
||||
outlk_ad.config_server = String::new();
|
||||
outlk_ad.config_port = 0;
|
||||
outlk_ad.config_ssl = String::new();
|
||||
outlk_ad.config_redirecturl = None;
|
||||
|
||||
break;
|
||||
}
|
||||
if Some(tag) == current_tag {
|
||||
current_tag = None;
|
||||
} else {
|
||||
current_tag = Some(tag);
|
||||
}
|
||||
}
|
||||
quick_xml::events::Event::End(ref e) => {
|
||||
outlk_autodiscover_endtag_cb(e, &mut outlk_ad);
|
||||
current_tag = None;
|
||||
}
|
||||
quick_xml::events::Event::Text(ref e) => {
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape_and_decode(&reader).unwrap_or_default();
|
||||
|
||||
if let Some(ref tag) = current_tag {
|
||||
match tag.as_str() {
|
||||
"type" => {
|
||||
outlk_ad.config_type = Some(val.trim().to_lowercase().to_string())
|
||||
"type" => protocol_type = Some(val.trim().to_string()),
|
||||
"server" => protocol_server = Some(val.trim().to_string()),
|
||||
"port" => protocol_port = Some(val.trim().parse().unwrap_or_default()),
|
||||
"ssl" => {
|
||||
protocol_ssl = match val.trim() {
|
||||
"on" => true,
|
||||
"off" => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
"server" => outlk_ad.config_server = val.trim().to_string(),
|
||||
"port" => outlk_ad.config_port = val.trim().parse().unwrap_or_default(),
|
||||
"ssl" => outlk_ad.config_ssl = val.trim().to_string(),
|
||||
"redirecturl" => outlk_ad.config_redirecturl = Some(val.trim().to_string()),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
quick_xml::events::Event::Eof => break,
|
||||
Event::Eof => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(protocol_type), Some(protocol_server), Some(protocol_port)) =
|
||||
(protocol_type, protocol_server, protocol_port)
|
||||
{
|
||||
Ok(Some(ProtocolTag {
|
||||
typ: protocol_type,
|
||||
server: protocol_server,
|
||||
port: protocol_port,
|
||||
ssl: protocol_ssl,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `RedirectUrl` tag.
|
||||
fn parse_redirecturl<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
) -> Result<String, quick_xml::Error> {
|
||||
let mut buf = Vec::new();
|
||||
match reader.read_event(&mut buf)? {
|
||||
Event::Text(ref e) => {
|
||||
let val = e.unescape_and_decode(&reader).unwrap_or_default();
|
||||
Ok(val.trim().to_string())
|
||||
}
|
||||
_ => Ok("".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_xml_reader<B: BufRead>(
|
||||
reader: &mut quick_xml::Reader<B>,
|
||||
) -> Result<ParsingResult, quick_xml::Error> {
|
||||
let mut protocols = Vec::new();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
match reader.read_event(&mut buf)? {
|
||||
Event::Start(ref e) => {
|
||||
let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase();
|
||||
|
||||
if tag == "protocol" {
|
||||
if let Some(protocol) = parse_protocol(reader)? {
|
||||
protocols.push(protocol);
|
||||
}
|
||||
} else if tag == "redirecturl" {
|
||||
let redirecturl = parse_redirecturl(reader)?;
|
||||
return Ok(ParsingResult::RedirectUrl(redirecturl));
|
||||
}
|
||||
}
|
||||
Event::Eof => break,
|
||||
_ => (),
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
// XML redirect via redirecturl
|
||||
let res = if outlk_ad.config_redirecturl.is_none()
|
||||
|| outlk_ad.config_redirecturl.as_ref().unwrap().is_empty()
|
||||
{
|
||||
if outlk_ad.out.mail_server.is_empty()
|
||||
|| outlk_ad.out.mail_port == 0
|
||||
|| outlk_ad.out.send_server.is_empty()
|
||||
|| outlk_ad.out.send_port == 0
|
||||
{
|
||||
return Err(Error::IncompleteAutoconfig(outlk_ad.out));
|
||||
}
|
||||
ParsingResult::LoginParam(outlk_ad.out)
|
||||
} else {
|
||||
ParsingResult::RedirectUrl(outlk_ad.config_redirecturl.unwrap())
|
||||
};
|
||||
Ok(res)
|
||||
Ok(ParsingResult::Protocols(protocols))
|
||||
}
|
||||
|
||||
pub async fn outlk_autodiscover(
|
||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
|
||||
let mut reader = quick_xml::Reader::from_str(&xml_raw);
|
||||
reader.trim_text(true);
|
||||
|
||||
parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
|
||||
position: reader.buffer_position(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
|
||||
protocols
|
||||
.into_iter()
|
||||
.filter_map(|protocol| {
|
||||
Some(ServerParams {
|
||||
protocol: match protocol.typ.to_lowercase().as_ref() {
|
||||
"imap" => Some(Protocol::IMAP),
|
||||
"smtp" => Some(Protocol::SMTP),
|
||||
_ => None,
|
||||
}?,
|
||||
socket: match protocol.ssl {
|
||||
true => Socket::Automatic,
|
||||
false => Socket::Plain,
|
||||
},
|
||||
hostname: protocol.server,
|
||||
port: protocol.port,
|
||||
username: String::new(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn outlk_autodiscover(
|
||||
context: &Context,
|
||||
url: &str,
|
||||
_param_in: &LoginParam,
|
||||
) -> Result<LoginParam, Error> {
|
||||
) -> Result<Vec<ServerParams>, Error> {
|
||||
let mut url = url.to_string();
|
||||
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
|
||||
for _i in 0..10 {
|
||||
@@ -127,43 +199,12 @@ pub async fn outlk_autodiscover(
|
||||
}
|
||||
match res? {
|
||||
ParsingResult::RedirectUrl(redirect_url) => url = redirect_url,
|
||||
ParsingResult::LoginParam(login_param) => return Ok(login_param),
|
||||
}
|
||||
}
|
||||
Err(Error::RedirectionError)
|
||||
}
|
||||
|
||||
fn outlk_autodiscover_endtag_cb(event: &BytesEnd, outlk_ad: &mut OutlookAutodiscover) {
|
||||
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
|
||||
|
||||
if tag == "protocol" {
|
||||
if let Some(type_) = &outlk_ad.config_type {
|
||||
let port = outlk_ad.config_port;
|
||||
let ssl_on = outlk_ad.config_ssl == "on";
|
||||
let ssl_off = outlk_ad.config_ssl == "off";
|
||||
if type_ == "imap" && !outlk_ad.out_imap_set {
|
||||
outlk_ad.out.mail_server =
|
||||
std::mem::replace(&mut outlk_ad.config_server, String::new());
|
||||
outlk_ad.out.mail_port = port;
|
||||
if ssl_on {
|
||||
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
|
||||
} else if ssl_off {
|
||||
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
|
||||
}
|
||||
outlk_ad.out_imap_set = true
|
||||
} else if type_ == "smtp" && !outlk_ad.out_smtp_set {
|
||||
outlk_ad.out.send_server =
|
||||
std::mem::replace(&mut outlk_ad.config_server, String::new());
|
||||
outlk_ad.out.send_port = outlk_ad.config_port;
|
||||
if ssl_on {
|
||||
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
|
||||
} else if ssl_off {
|
||||
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
|
||||
}
|
||||
outlk_ad.out_smtp_set = true
|
||||
ParsingResult::Protocols(protocols) => {
|
||||
return Ok(protocols_to_serverparams(protocols));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(Error::RedirectionError)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -184,16 +225,13 @@ mod tests {
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
").expect("XML is not parsed successfully");
|
||||
match res {
|
||||
ParsingResult::LoginParam(_lp) => {
|
||||
panic!("redirecturl is not found");
|
||||
}
|
||||
ParsingResult::RedirectUrl(url) => {
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://mail.example.com/autodiscover/autodiscover.xml"
|
||||
);
|
||||
}
|
||||
if let ParsingResult::RedirectUrl(url) = res {
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://mail.example.com/autodiscover/autodiscover.xml"
|
||||
);
|
||||
} else {
|
||||
panic!("redirecturl is not found");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,11 +266,16 @@ mod tests {
|
||||
.expect("XML is not parsed successfully");
|
||||
|
||||
match res {
|
||||
ParsingResult::LoginParam(lp) => {
|
||||
assert_eq!(lp.mail_server, "example.com");
|
||||
assert_eq!(lp.mail_port, 993);
|
||||
assert_eq!(lp.send_server, "smtp.example.com");
|
||||
assert_eq!(lp.send_port, 25);
|
||||
ParsingResult::Protocols(protocols) => {
|
||||
assert_eq!(protocols[0].typ, "IMAP");
|
||||
assert_eq!(protocols[0].server, "example.com");
|
||||
assert_eq!(protocols[0].port, 993);
|
||||
assert_eq!(protocols[0].ssl, true);
|
||||
|
||||
assert_eq!(protocols[1].typ, "SMTP");
|
||||
assert_eq!(protocols[1].server, "smtp.example.com");
|
||||
assert_eq!(protocols[1].port, 25);
|
||||
assert_eq!(protocols[1].ssl, false);
|
||||
}
|
||||
ParsingResult::RedirectUrl(_) => {
|
||||
panic!("RedirectUrl is not expected");
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
mod auto_mozilla;
|
||||
mod auto_outlook;
|
||||
mod read_url;
|
||||
mod server_params;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::prelude::*;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
@@ -13,14 +14,16 @@ use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::imap::Imap;
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::login_param::{LoginParam, ServerLoginParam};
|
||||
use crate::message::Message;
|
||||
use crate::oauth2::*;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::{chat, e2ee, provider};
|
||||
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use server_params::ServerParams;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr) => {
|
||||
@@ -28,7 +31,7 @@ macro_rules! progress {
|
||||
$progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.emit_event($crate::events::Event::ConfigureProgress($progress));
|
||||
$context.emit_event($crate::events::EventType::ConfigureProgress($progress));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,20 +118,37 @@ impl Context {
|
||||
}
|
||||
|
||||
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
let mut param_autoconfig: Option<LoginParam> = None;
|
||||
let mut keep_flags = 0;
|
||||
|
||||
// Read login parameters from the database
|
||||
progress!(ctx, 1);
|
||||
|
||||
// Check basic settings.
|
||||
ensure!(!param.addr.is_empty(), "Please enter an email address.");
|
||||
|
||||
// Only check for IMAP password, SMTP password is an "advanced" setting.
|
||||
ensure!(!param.imap.password.is_empty(), "Please enter a password.");
|
||||
if param.smtp.password.is_empty() {
|
||||
param.smtp.password = param.imap.password.clone()
|
||||
}
|
||||
|
||||
// Normalize authentication flags.
|
||||
let oauth2 = match param.server_flags & DC_LP_AUTH_FLAGS as i32 {
|
||||
DC_LP_AUTH_OAUTH2 => true,
|
||||
DC_LP_AUTH_NORMAL => false,
|
||||
_ => false,
|
||||
};
|
||||
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
|
||||
param.server_flags |= if oauth2 {
|
||||
DC_LP_AUTH_OAUTH2 as i32
|
||||
} else {
|
||||
DC_LP_AUTH_NORMAL as i32
|
||||
};
|
||||
|
||||
// Step 1: Load the parameters and check email-address and password
|
||||
|
||||
if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 {
|
||||
if oauth2 {
|
||||
// the used oauth2 addr may differ, check this.
|
||||
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
|
||||
progress!(ctx, 10);
|
||||
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, ¶m.addr, ¶m.mail_pw)
|
||||
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password)
|
||||
.await
|
||||
.and_then(|e| e.parse().ok())
|
||||
{
|
||||
@@ -149,130 +169,106 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
// Step 2: Autoconfig
|
||||
progress!(ctx, 200);
|
||||
|
||||
// param.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then
|
||||
// param.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for
|
||||
// autoconfig or not
|
||||
if param.mail_server.is_empty()
|
||||
&& param.mail_port == 0
|
||||
&& param.send_server.is_empty()
|
||||
&& param.send_port == 0
|
||||
&& param.send_user.is_empty()
|
||||
&& (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0
|
||||
let param_autoconfig;
|
||||
if param.imap.server.is_empty()
|
||||
&& param.imap.port == 0
|
||||
&& param.imap.security == Socket::Automatic
|
||||
&& param.imap.user.is_empty()
|
||||
&& param.smtp.server.is_empty()
|
||||
&& param.smtp.port == 0
|
||||
&& param.smtp.security == Socket::Automatic
|
||||
&& param.smtp.user.is_empty()
|
||||
{
|
||||
// no advanced parameters entered by the user: query provider-database or do Autoconfig
|
||||
keep_flags = param.server_flags & DC_LP_AUTH_OAUTH2;
|
||||
if let Some(new_param) = get_offline_autoconfig(ctx, ¶m) {
|
||||
// got parameters from our provider-database, skip Autoconfig, preserve the OAuth2 setting
|
||||
param_autoconfig = Some(new_param);
|
||||
}
|
||||
|
||||
if param_autoconfig.is_none() {
|
||||
if let Some(servers) = get_offline_autoconfig(ctx, ¶m.addr) {
|
||||
param_autoconfig = Some(servers);
|
||||
} else {
|
||||
param_autoconfig =
|
||||
get_autoconfig(ctx, param, ¶m_domain, ¶m_addr_urlencoded).await;
|
||||
}
|
||||
} else {
|
||||
param_autoconfig = None;
|
||||
}
|
||||
|
||||
// C. Do we have any autoconfig result?
|
||||
progress!(ctx, 500);
|
||||
if let Some(ref cfg) = param_autoconfig {
|
||||
info!(ctx, "Got autoconfig: {}", &cfg);
|
||||
if !cfg.mail_user.is_empty() {
|
||||
param.mail_user = cfg.mail_user.clone();
|
||||
}
|
||||
// all other values are always NULL when entering autoconfig
|
||||
param.mail_server = cfg.mail_server.clone();
|
||||
param.mail_port = cfg.mail_port;
|
||||
param.send_server = cfg.send_server.clone();
|
||||
param.send_port = cfg.send_port;
|
||||
param.send_user = cfg.send_user.clone();
|
||||
param.server_flags = cfg.server_flags;
|
||||
// although param_autoconfig's data are no longer needed from,
|
||||
// it is used to later to prevent trying variations of port/server/logins
|
||||
}
|
||||
param.server_flags |= keep_flags;
|
||||
|
||||
// Step 3: Fill missing fields with defaults
|
||||
if param.mail_server.is_empty() {
|
||||
param.mail_server = format!("imap.{}", param_domain,)
|
||||
}
|
||||
if param.mail_port == 0 {
|
||||
param.mail_port = if 0 != param.server_flags & (0x100 | 0x400) {
|
||||
143
|
||||
} else {
|
||||
993
|
||||
}
|
||||
}
|
||||
if param.mail_user.is_empty() {
|
||||
param.mail_user = param.addr.clone();
|
||||
}
|
||||
if param.send_server.is_empty() && !param.mail_server.is_empty() {
|
||||
param.send_server = param.mail_server.clone();
|
||||
if param.send_server.starts_with("imap.") {
|
||||
param.send_server = param.send_server.replacen("imap", "smtp", 1);
|
||||
}
|
||||
}
|
||||
if param.send_port == 0 {
|
||||
param.send_port = if 0 != param.server_flags & DC_LP_SMTP_SOCKET_STARTTLS as i32 {
|
||||
587
|
||||
} else if 0 != param.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32 {
|
||||
25
|
||||
} else {
|
||||
465
|
||||
}
|
||||
}
|
||||
if param.send_user.is_empty() && !param.mail_user.is_empty() {
|
||||
param.send_user = param.mail_user.clone();
|
||||
}
|
||||
if param.send_pw.is_empty() && !param.mail_pw.is_empty() {
|
||||
param.send_pw = param.mail_pw.clone()
|
||||
}
|
||||
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_AUTH_FLAGS as i32) {
|
||||
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_AUTH_NORMAL as i32
|
||||
}
|
||||
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_IMAP_SOCKET_FLAGS as i32) {
|
||||
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= if param.mail_port == 143 {
|
||||
DC_LP_IMAP_SOCKET_STARTTLS as i32
|
||||
} else {
|
||||
DC_LP_IMAP_SOCKET_SSL as i32
|
||||
}
|
||||
}
|
||||
if !dc_exactly_one_bit_set(param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32)) {
|
||||
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= if param.send_port == 587 {
|
||||
DC_LP_SMTP_SOCKET_STARTTLS as i32
|
||||
} else if param.send_port == 25 {
|
||||
DC_LP_SMTP_SOCKET_PLAIN as i32
|
||||
} else {
|
||||
DC_LP_SMTP_SOCKET_SSL as i32
|
||||
}
|
||||
}
|
||||
|
||||
// do we have a complete configuration?
|
||||
ensure!(
|
||||
!param.mail_server.is_empty()
|
||||
&& param.mail_port != 0
|
||||
&& !param.mail_user.is_empty()
|
||||
&& !param.mail_pw.is_empty()
|
||||
&& !param.send_server.is_empty()
|
||||
&& param.send_port != 0
|
||||
&& !param.send_user.is_empty()
|
||||
&& !param.send_pw.is_empty()
|
||||
&& param.server_flags != 0,
|
||||
"Account settings incomplete."
|
||||
);
|
||||
let servers: Vec<ServerParams> = param_autoconfig
|
||||
.unwrap_or_else(|| {
|
||||
vec![
|
||||
ServerParams {
|
||||
protocol: Protocol::IMAP,
|
||||
hostname: param.imap.server.clone(),
|
||||
port: param.imap.port,
|
||||
socket: param.imap.security,
|
||||
username: param.imap.user.clone(),
|
||||
},
|
||||
ServerParams {
|
||||
protocol: Protocol::SMTP,
|
||||
hostname: param.smtp.server.clone(),
|
||||
port: param.smtp.port,
|
||||
socket: param.smtp.security,
|
||||
username: param.smtp.user.clone(),
|
||||
},
|
||||
]
|
||||
})
|
||||
.into_iter()
|
||||
// The order of expansion is important: ports are expanded the
|
||||
// last, so they are changed the first. Username is only
|
||||
// changed if default value (address with domain) didn't work
|
||||
// for all available hosts and ports.
|
||||
.flat_map(|params| params.expand_usernames(¶m.addr).into_iter())
|
||||
.flat_map(|params| params.expand_hostnames(¶m_domain).into_iter())
|
||||
.flat_map(|params| params.expand_ports().into_iter())
|
||||
.collect();
|
||||
|
||||
// Configure IMAP
|
||||
progress!(ctx, 600);
|
||||
// try to connect to IMAP - if we did not got an autoconfig,
|
||||
// do some further tries with different settings and username variations
|
||||
let (_s, r) = async_std::sync::channel(1);
|
||||
let mut imap = Imap::new(r);
|
||||
|
||||
try_imap_connections(ctx, param, param_autoconfig.is_some(), &mut imap).await?;
|
||||
progress!(ctx, 800);
|
||||
let mut imap_configured = false;
|
||||
for imap_server in servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::IMAP)
|
||||
{
|
||||
param.imap.user = imap_server.username.clone();
|
||||
param.imap.server = imap_server.hostname.clone();
|
||||
param.imap.port = imap_server.port;
|
||||
param.imap.security = imap_server.socket;
|
||||
|
||||
if try_imap_one_param(ctx, ¶m.imap, ¶m.addr, oauth2, &mut imap).await {
|
||||
imap_configured = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !imap_configured {
|
||||
bail!("IMAP autoconfig did not succeed");
|
||||
}
|
||||
|
||||
// Configure SMTP
|
||||
progress!(ctx, 750);
|
||||
let mut smtp = Smtp::new();
|
||||
|
||||
let mut smtp_configured = false;
|
||||
for smtp_server in servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::SMTP)
|
||||
{
|
||||
param.smtp.user = smtp_server.username.clone();
|
||||
param.smtp.server = smtp_server.hostname.clone();
|
||||
param.smtp.port = smtp_server.port;
|
||||
param.smtp.security = smtp_server.socket;
|
||||
|
||||
if try_smtp_one_param(ctx, ¶m.smtp, ¶m.addr, oauth2, &mut smtp).await {
|
||||
smtp_configured = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !smtp_configured {
|
||||
bail!("SMTP autoconfig did not succeed");
|
||||
}
|
||||
|
||||
try_smtp_connections(ctx, param, param_autoconfig.is_some()).await?;
|
||||
progress!(ctx, 900);
|
||||
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
|
||||
@@ -341,8 +337,8 @@ impl AutoconfigSource {
|
||||
AutoconfigSource {
|
||||
provider: AutoconfigProvider::Outlook,
|
||||
url: format!(
|
||||
"https://{}{}/autodiscover/autodiscover.xml",
|
||||
"autodiscover.", domain
|
||||
"https://autodiscover.{}/autodiscover/autodiscover.xml",
|
||||
domain
|
||||
),
|
||||
},
|
||||
// always SSL for Thunderbird's database
|
||||
@@ -353,10 +349,10 @@ impl AutoconfigSource {
|
||||
]
|
||||
}
|
||||
|
||||
async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result<LoginParam> {
|
||||
async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result<Vec<ServerParams>> {
|
||||
let params = match self.provider {
|
||||
AutoconfigProvider::Mozilla => moz_autoconfigure(ctx, &self.url, ¶m).await?,
|
||||
AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url, ¶m).await?,
|
||||
AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url).await?,
|
||||
};
|
||||
|
||||
Ok(params)
|
||||
@@ -372,7 +368,7 @@ async fn get_autoconfig(
|
||||
param: &LoginParam,
|
||||
param_domain: &str,
|
||||
param_addr_urlencoded: &str,
|
||||
) -> Option<LoginParam> {
|
||||
) -> Option<Vec<ServerParams>> {
|
||||
let sources = AutoconfigSource::all(param_domain, param_addr_urlencoded);
|
||||
|
||||
let mut progress = 300;
|
||||
@@ -388,215 +384,97 @@ async fn get_autoconfig(
|
||||
None
|
||||
}
|
||||
|
||||
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
|
||||
fn get_offline_autoconfig(context: &Context, addr: &str) -> Option<Vec<ServerParams>> {
|
||||
info!(
|
||||
context,
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
if let Some(provider) = provider::get_provider_info(¶m.addr) {
|
||||
if let Some(provider) = provider::get_provider_info(&addr) {
|
||||
match provider.status {
|
||||
provider::Status::OK | provider::Status::PREPARATION => {
|
||||
let imap = provider.get_imap_server();
|
||||
let smtp = provider.get_smtp_server();
|
||||
// clippy complains about these is_some()/unwrap() settings,
|
||||
// however, rewriting the code to "if let" would make things less obvious,
|
||||
// esp. if we allow more combinations of servers (pop, jmap).
|
||||
// therefore, #[allow(clippy::unnecessary_unwrap)] is added above.
|
||||
if let Some(imap) = imap {
|
||||
if let Some(smtp) = smtp {
|
||||
let mut p = LoginParam::new();
|
||||
p.addr = param.addr.clone();
|
||||
|
||||
p.mail_server = imap.hostname.to_string();
|
||||
p.mail_user = imap.apply_username_pattern(param.addr.clone());
|
||||
p.mail_port = imap.port as i32;
|
||||
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
|
||||
p.server_flags |= match imap.socket {
|
||||
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
|
||||
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
|
||||
};
|
||||
|
||||
p.send_server = smtp.hostname.to_string();
|
||||
p.send_user = smtp.apply_username_pattern(param.addr.clone());
|
||||
p.send_port = smtp.port as i32;
|
||||
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
|
||||
p.server_flags |= match smtp.socket {
|
||||
provider::Socket::STARTTLS => DC_LP_SMTP_SOCKET_STARTTLS as i32,
|
||||
provider::Socket::SSL => DC_LP_SMTP_SOCKET_SSL as i32,
|
||||
};
|
||||
|
||||
info!(context, "offline autoconfig found: {}", p);
|
||||
return Some(p);
|
||||
}
|
||||
if provider.server.is_empty() {
|
||||
info!(context, "offline autoconfig found, but no servers defined");
|
||||
None
|
||||
} else {
|
||||
info!(context, "offline autoconfig found");
|
||||
let servers = provider
|
||||
.server
|
||||
.iter()
|
||||
.map(|s| ServerParams {
|
||||
protocol: s.protocol,
|
||||
socket: s.socket,
|
||||
hostname: s.hostname.to_string(),
|
||||
port: s.port,
|
||||
username: match s.username_pattern {
|
||||
UsernamePattern::EMAIL => addr.to_string(),
|
||||
UsernamePattern::EMAILLOCALPART => {
|
||||
if let Some(at) = addr.find('@') {
|
||||
addr.split_at(at).0.to_string()
|
||||
} else {
|
||||
addr.to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
Some(servers)
|
||||
}
|
||||
info!(context, "offline autoconfig found, but no servers defined");
|
||||
return None;
|
||||
}
|
||||
provider::Status::BROKEN => {
|
||||
info!(context, "offline autoconfig found, provider is broken");
|
||||
return None;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
info!(context, "no offline autoconfig found");
|
||||
None
|
||||
}
|
||||
|
||||
async fn try_imap_connections(
|
||||
context: &Context,
|
||||
param: &mut LoginParam,
|
||||
was_autoconfig: bool,
|
||||
imap: &mut Imap,
|
||||
) -> Result<()> {
|
||||
// manually_set_param is used to check whether a particular setting was set manually by the user.
|
||||
// If yes, we do not want to change it to avoid confusing error messages
|
||||
// (you set port 443, but the app tells you it couldn't connect on port 993).
|
||||
let manually_set_param = LoginParam::from_database(context, "").await;
|
||||
|
||||
// progress 650 and 660
|
||||
if try_imap_connection(context, param, &manually_set_param, was_autoconfig, 0, imap)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
bail!("autoconfig did not succeed");
|
||||
}
|
||||
|
||||
progress!(context, 670);
|
||||
// try_imap_connection() changed the flags and port. Change them back:
|
||||
if manually_set_param.server_flags & DC_LP_IMAP_SOCKET_FLAGS == 0 {
|
||||
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
|
||||
param.server_flags |= DC_LP_IMAP_SOCKET_SSL;
|
||||
}
|
||||
if manually_set_param.mail_port == 0 {
|
||||
param.mail_port = 993;
|
||||
}
|
||||
if let Some(at) = param.mail_user.find('@') {
|
||||
param.mail_user = param.mail_user.split_at(at).0.to_string();
|
||||
}
|
||||
if let Some(at) = param.send_user.find('@') {
|
||||
param.send_user = param.send_user.split_at(at).0.to_string();
|
||||
}
|
||||
// progress 680 and 690
|
||||
try_imap_connection(context, param, &manually_set_param, was_autoconfig, 1, imap).await
|
||||
}
|
||||
|
||||
async fn try_imap_connection(
|
||||
context: &Context,
|
||||
param: &mut LoginParam,
|
||||
manually_set_param: &LoginParam,
|
||||
was_autoconfig: bool,
|
||||
variation: usize,
|
||||
imap: &mut Imap,
|
||||
) -> Result<()> {
|
||||
if try_imap_one_param(context, param, imap).await.is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
bail!("autoconfig did not succeed");
|
||||
}
|
||||
|
||||
progress!(context, 650 + variation * 30);
|
||||
|
||||
if manually_set_param.server_flags & DC_LP_IMAP_SOCKET_FLAGS == 0 {
|
||||
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
|
||||
param.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS;
|
||||
|
||||
if try_imap_one_param(context, ¶m, imap).await.is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
progress!(context, 660 + variation * 30);
|
||||
|
||||
if manually_set_param.mail_port == 0 {
|
||||
param.mail_port = 143;
|
||||
try_imap_one_param(context, param, imap).await
|
||||
} else {
|
||||
Err(format_err!("no more possible configs"))
|
||||
info!(context, "no offline autoconfig found");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_imap_one_param(context: &Context, param: &LoginParam, imap: &mut Imap) -> Result<()> {
|
||||
let inf = format!(
|
||||
"imap: {}@{}:{} flags=0x{:x} certificate_checks={}",
|
||||
param.mail_user,
|
||||
param.mail_server,
|
||||
param.mail_port,
|
||||
param.server_flags,
|
||||
param.imap_certificate_checks
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if imap.connect(context, ¶m).await {
|
||||
info!(context, "success: {}", inf);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
bail!("Could not connect: {}", inf);
|
||||
}
|
||||
|
||||
async fn try_smtp_connections(
|
||||
async fn try_imap_one_param(
|
||||
context: &Context,
|
||||
param: &mut LoginParam,
|
||||
was_autoconfig: bool,
|
||||
) -> Result<()> {
|
||||
// manually_set_param is used to check whether a particular setting was set manually by the user.
|
||||
// If yes, we do not want to change it to avoid confusing error messages
|
||||
// (you set port 443, but the app tells you it couldn't connect on port 993).
|
||||
let manually_set_param = LoginParam::from_database(context, "").await;
|
||||
|
||||
let mut smtp = Smtp::new();
|
||||
// try to connect to SMTP - if we did not got an autoconfig, the first try was SSL-465 and we do
|
||||
// a second try with STARTTLS-587
|
||||
if try_smtp_one_param(context, param, &mut smtp).await.is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
if was_autoconfig {
|
||||
bail!("No SMTP connection");
|
||||
}
|
||||
progress!(context, 850);
|
||||
|
||||
if manually_set_param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32) == 0 {
|
||||
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
|
||||
}
|
||||
if manually_set_param.send_port == 0 {
|
||||
param.send_port = 587;
|
||||
}
|
||||
|
||||
if try_smtp_one_param(context, param, &mut smtp).await.is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
progress!(context, 860);
|
||||
|
||||
if manually_set_param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32) == 0 {
|
||||
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
|
||||
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
|
||||
}
|
||||
if manually_set_param.send_port == 0 {
|
||||
param.send_port = 25;
|
||||
}
|
||||
try_smtp_one_param(context, param, &mut smtp).await
|
||||
}
|
||||
|
||||
async fn try_smtp_one_param(context: &Context, param: &LoginParam, smtp: &mut Smtp) -> Result<()> {
|
||||
param: &ServerLoginParam,
|
||||
addr: &str,
|
||||
oauth2: bool,
|
||||
imap: &mut Imap,
|
||||
) -> bool {
|
||||
let inf = format!(
|
||||
"smtp: {}@{}:{} flags: 0x{:x}",
|
||||
param.send_user, param.send_server, param.send_port, param.server_flags
|
||||
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
|
||||
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if let Err(err) = smtp.connect(context, ¶m).await {
|
||||
bail!("could not connect: {}", err);
|
||||
if let Err(err) = imap.connect(context, param, addr, oauth2).await {
|
||||
info!(context, "failure: {}", err);
|
||||
false
|
||||
} else {
|
||||
info!(context, "success: {}", inf);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "success: {}", inf);
|
||||
smtp.disconnect().await;
|
||||
Ok(())
|
||||
async fn try_smtp_one_param(
|
||||
context: &Context,
|
||||
param: &ServerLoginParam,
|
||||
addr: &str,
|
||||
oauth2: bool,
|
||||
smtp: &mut Smtp,
|
||||
) -> bool {
|
||||
let inf = format!(
|
||||
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={}",
|
||||
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if let Err(err) = smtp.connect(context, param, addr, oauth2).await {
|
||||
info!(context, "failure: {}", err);
|
||||
false
|
||||
} else {
|
||||
info!(context, "success: {}", inf);
|
||||
smtp.disconnect().await;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -611,9 +489,6 @@ pub enum Error {
|
||||
error: quick_xml::Error,
|
||||
},
|
||||
|
||||
#[error("Bad or incomplete autoconfig")]
|
||||
IncompleteAutoconfig(LoginParam),
|
||||
|
||||
#[error("Failed to get URL")]
|
||||
ReadUrlError(#[from] self::read_url::Error),
|
||||
|
||||
@@ -646,14 +521,15 @@ mod tests {
|
||||
async fn test_get_offline_autoconfig() {
|
||||
let context = TestContext::new().await.ctx;
|
||||
|
||||
let mut params = LoginParam::new();
|
||||
params.addr = "someone123@example.org".to_string();
|
||||
assert!(get_offline_autoconfig(&context, ¶ms).is_none());
|
||||
let addr = "someone123@example.org";
|
||||
assert!(get_offline_autoconfig(&context, addr).is_none());
|
||||
|
||||
let mut params = LoginParam::new();
|
||||
params.addr = "someone123@nauta.cu".to_string();
|
||||
let found_params = get_offline_autoconfig(&context, ¶ms).unwrap();
|
||||
assert_eq!(found_params.mail_server, "imap.nauta.cu".to_string());
|
||||
assert_eq!(found_params.send_server, "smtp.nauta.cu".to_string());
|
||||
let addr = "someone123@nauta.cu";
|
||||
let found_params = get_offline_autoconfig(&context, addr).unwrap();
|
||||
assert_eq!(found_params.len(), 2);
|
||||
assert_eq!(found_params[0].protocol, Protocol::IMAP);
|
||||
assert_eq!(found_params[0].hostname, "imap.nauta.cu".to_string());
|
||||
assert_eq!(found_params[1].protocol, Protocol::SMTP);
|
||||
assert_eq!(found_params[1].hostname, "smtp.nauta.cu".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
115
src/configure/server_params.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
//! Variable server parameters lists
|
||||
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
/// Set of variable parameters to try during configuration.
|
||||
///
|
||||
/// Can be loaded from offline provider database, online configuraiton
|
||||
/// or derived from user entered parameters.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ServerParams {
|
||||
/// Protocol, such as IMAP or SMTP.
|
||||
pub protocol: Protocol,
|
||||
|
||||
/// Server hostname, empty if unknown.
|
||||
pub hostname: String,
|
||||
|
||||
/// Server port, zero if unknown.
|
||||
pub port: u16,
|
||||
|
||||
/// Socket security, such as TLS or STARTTLS, Socket::Automatic if unknown.
|
||||
pub socket: Socket,
|
||||
|
||||
/// Username, empty if unknown.
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl ServerParams {
|
||||
pub(crate) fn expand_usernames(mut self, addr: &str) -> Vec<ServerParams> {
|
||||
let mut res = Vec::new();
|
||||
|
||||
if self.username.is_empty() {
|
||||
self.username = addr.to_string();
|
||||
res.push(self.clone());
|
||||
|
||||
if let Some(at) = addr.find('@') {
|
||||
self.username = addr.split_at(at).0.to_string();
|
||||
res.push(self);
|
||||
}
|
||||
} else {
|
||||
res.push(self)
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn expand_hostnames(mut self, param_domain: &str) -> Vec<ServerParams> {
|
||||
let mut res = Vec::new();
|
||||
if self.hostname.is_empty() {
|
||||
self.hostname = param_domain.to_string();
|
||||
res.push(self.clone());
|
||||
|
||||
self.hostname = match self.protocol {
|
||||
Protocol::IMAP => "imap.".to_string() + param_domain,
|
||||
Protocol::SMTP => "smtp.".to_string() + param_domain,
|
||||
};
|
||||
res.push(self.clone());
|
||||
|
||||
self.hostname = "mail.".to_string() + param_domain;
|
||||
res.push(self);
|
||||
} else {
|
||||
res.push(self);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn expand_ports(mut self) -> Vec<ServerParams> {
|
||||
// Try to infer port from socket security.
|
||||
if self.port == 0 {
|
||||
self.port = match self.socket {
|
||||
Socket::SSL => match self.protocol {
|
||||
Protocol::IMAP => 993,
|
||||
Protocol::SMTP => 465,
|
||||
},
|
||||
Socket::STARTTLS | Socket::Plain => match self.protocol {
|
||||
Protocol::IMAP => 143,
|
||||
Protocol::SMTP => 587,
|
||||
},
|
||||
Socket::Automatic => 0,
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = Vec::new();
|
||||
if self.port == 0 {
|
||||
// Neither port nor security is set.
|
||||
//
|
||||
// Try common secure combinations.
|
||||
|
||||
// Try STARTTLS
|
||||
self.socket = Socket::STARTTLS;
|
||||
self.port = match self.protocol {
|
||||
Protocol::IMAP => 143,
|
||||
Protocol::SMTP => 587,
|
||||
};
|
||||
res.push(self.clone());
|
||||
|
||||
// Try TLS
|
||||
self.socket = Socket::SSL;
|
||||
self.port = match self.protocol {
|
||||
Protocol::IMAP => 993,
|
||||
Protocol::SMTP => 465,
|
||||
};
|
||||
res.push(self);
|
||||
} else if self.socket == Socket::Automatic {
|
||||
// Try TLS over user-provided port.
|
||||
self.socket = Socket::SSL;
|
||||
res.push(self.clone());
|
||||
|
||||
// Try STARTTLS over user-provided port.
|
||||
self.socket = Socket::STARTTLS;
|
||||
res.push(self);
|
||||
} else {
|
||||
res.push(self);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
@@ -199,44 +199,8 @@ pub const DC_LP_AUTH_OAUTH2: i32 = 0x2;
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
|
||||
|
||||
/// Connect to IMAP via STARTTLS.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_IMAP_SOCKET_STARTTLS: i32 = 0x100;
|
||||
|
||||
/// Connect to IMAP via SSL.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_IMAP_SOCKET_SSL: i32 = 0x200;
|
||||
|
||||
/// Connect to IMAP unencrypted, this should not be used.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_IMAP_SOCKET_PLAIN: i32 = 0x400;
|
||||
|
||||
/// Connect to SMTP via STARTTLS.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_SMTP_SOCKET_STARTTLS: usize = 0x10000;
|
||||
|
||||
/// Connect to SMTP via SSL.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
|
||||
|
||||
/// Connect to SMTP unencrypted, this should not be used.
|
||||
/// If this flag is set, automatic configuration is skipped.
|
||||
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
|
||||
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
|
||||
DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN;
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
|
||||
DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN;
|
||||
|
||||
// QR code scanning (view from Bob, the joiner)
|
||||
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
|
||||
pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
|
||||
pub const DC_BOB_ERROR: i32 = 0;
|
||||
pub const DC_BOB_SUCCESS: i32 = 1;
|
||||
|
||||
// max. width/height of an avatar
|
||||
pub const AVATAR_SIZE: u32 = 192;
|
||||
|
||||
@@ -13,13 +13,14 @@ use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::{bail, ensure, format_err, Result};
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::{MessageState, MsgId};
|
||||
use crate::mimeparser::AvatarAction;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::provider::Socket;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
/// An object representing a single contact in memory.
|
||||
@@ -235,6 +236,7 @@ impl Contact {
|
||||
name: impl AsRef<str>,
|
||||
addr: impl AsRef<str>,
|
||||
) -> Result<u32> {
|
||||
let name = improve_single_line_input(name);
|
||||
ensure!(
|
||||
!addr.as_ref().is_empty(),
|
||||
"Cannot create contact with empty address"
|
||||
@@ -245,7 +247,7 @@ impl Contact {
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(context, name, addr, Origin::ManuallyCreated).await?;
|
||||
let blocked = Contact::is_blocked_load(context, contact_id).await;
|
||||
context.emit_event(Event::ContactsChanged(
|
||||
context.emit_event(EventType::ContactsChanged(
|
||||
if sth_modified == Modifier::Created {
|
||||
Some(contact_id)
|
||||
} else {
|
||||
@@ -273,7 +275,7 @@ impl Contact {
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
@@ -533,7 +535,7 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
if modify_cnt > 0 {
|
||||
context.emit_event(Event::ContactsChanged(None));
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
}
|
||||
|
||||
Ok(modify_cnt)
|
||||
@@ -678,7 +680,7 @@ impl Contact {
|
||||
let mut ret = String::new();
|
||||
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await;
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
|
||||
let loginparam = LoginParam::from_database(context, "configured_").await;
|
||||
|
||||
if peerstate.is_some()
|
||||
@@ -728,8 +730,8 @@ impl Contact {
|
||||
);
|
||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||
}
|
||||
} else if 0 == loginparam.server_flags & DC_LP_IMAP_SOCKET_PLAIN as i32
|
||||
&& 0 == loginparam.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32
|
||||
} else if loginparam.imap.security != Socket::Plain
|
||||
&& loginparam.smtp.security != Socket::Plain
|
||||
{
|
||||
ret += &context.stock_str(StockMessage::EncrTransp).await;
|
||||
} else {
|
||||
@@ -784,7 +786,7 @@ impl Contact {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
context.emit_event(Event::ContactsChanged(None));
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -939,7 +941,17 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
let peerstate = Peerstate::from_addr(context, &self.addr).await;
|
||||
let peerstate = match Peerstate::from_addr(context, &self.addr).await {
|
||||
Ok(peerstate) => peerstate,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to load peerstate for address {}: {}", self.addr, err
|
||||
);
|
||||
return VerifiedStatus::Unverified;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ps) = peerstate {
|
||||
if ps.verified_key.is_some() {
|
||||
return VerifiedStatus::BidirectVerified;
|
||||
@@ -1083,7 +1095,7 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
|
||||
paramsv![new_blocking, 100, contact_id as i32],
|
||||
).await.is_ok() {
|
||||
Contact::mark_noticed(context, contact_id).await;
|
||||
context.emit_event(Event::ContactsChanged(None));
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1133,7 +1145,7 @@ pub(crate) async fn set_profile_image(
|
||||
};
|
||||
if changed {
|
||||
contact.update_param(context).await?;
|
||||
context.emit_event(Event::ContactsChanged(Some(contact_id)));
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::dc_tools::duration_to_str;
|
||||
use crate::error::*;
|
||||
use crate::events::{Event, EventEmitter, Events};
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::securejoin::Bob;
|
||||
use crate::sql::Sql;
|
||||
use std::time::SystemTime;
|
||||
|
||||
@@ -44,7 +44,7 @@ pub struct InnerContext {
|
||||
pub(crate) blobdir: PathBuf,
|
||||
pub(crate) sql: Sql,
|
||||
pub(crate) os_name: Option<String>,
|
||||
pub(crate) bob: RwLock<BobStatus>,
|
||||
pub(crate) bob: RwLock<Bob>,
|
||||
pub(crate) last_smeared_timestamp: RwLock<i64>,
|
||||
pub(crate) running_state: RwLock<RunningState>,
|
||||
/// Mutex to avoid generating the key for the user more than once.
|
||||
@@ -59,6 +59,9 @@ pub struct InnerContext {
|
||||
pub(crate) scheduler: RwLock<Scheduler>,
|
||||
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
|
||||
|
||||
/// Id for this context on the current device.
|
||||
pub(crate) id: u32,
|
||||
|
||||
creation_time: SystemTime,
|
||||
}
|
||||
|
||||
@@ -86,7 +89,7 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
|
||||
impl Context {
|
||||
/// Creates new context.
|
||||
pub async fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
|
||||
pub async fn new(os_name: String, dbfile: PathBuf, id: u32) -> Result<Context> {
|
||||
// pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
let mut blob_fname = OsString::new();
|
||||
@@ -96,13 +99,14 @@ impl Context {
|
||||
if !blobdir.exists().await {
|
||||
async_std::fs::create_dir_all(&blobdir).await?;
|
||||
}
|
||||
Context::with_blobdir(os_name, dbfile, blobdir).await
|
||||
Context::with_blobdir(os_name, dbfile, blobdir, id).await
|
||||
}
|
||||
|
||||
pub async fn with_blobdir(
|
||||
pub(crate) async fn with_blobdir(
|
||||
os_name: String,
|
||||
dbfile: PathBuf,
|
||||
blobdir: PathBuf,
|
||||
id: u32,
|
||||
) -> Result<Context> {
|
||||
ensure!(
|
||||
blobdir.is_dir().await,
|
||||
@@ -111,6 +115,7 @@ impl Context {
|
||||
);
|
||||
|
||||
let inner = InnerContext {
|
||||
id,
|
||||
blobdir,
|
||||
dbfile,
|
||||
os_name: Some(os_name),
|
||||
@@ -188,8 +193,11 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Emits a single event.
|
||||
pub fn emit_event(&self, event: Event) {
|
||||
self.events.emit(event);
|
||||
pub fn emit_event(&self, event: EventType) {
|
||||
self.events.emit(Event {
|
||||
id: self.id,
|
||||
typ: event,
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the next queued event.
|
||||
@@ -197,6 +205,11 @@ impl Context {
|
||||
self.events.get_emitter()
|
||||
}
|
||||
|
||||
/// Get the ID of this context.
|
||||
pub fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
// Ongoing process allocation/free/check
|
||||
|
||||
pub async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
|
||||
@@ -458,6 +471,13 @@ impl Context {
|
||||
self.get_config(Config::ConfiguredMvboxFolder).await
|
||||
== Some(folder_name.as_ref().to_string())
|
||||
}
|
||||
|
||||
pub 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)
|
||||
}
|
||||
}
|
||||
|
||||
impl InnerContext {
|
||||
@@ -488,13 +508,6 @@ impl Default for RunningState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct BobStatus {
|
||||
pub expects: i32,
|
||||
pub status: i32,
|
||||
pub qr_scan: Option<Lot>,
|
||||
}
|
||||
|
||||
pub fn get_version_str() -> &'static str {
|
||||
&DC_VERSION_STR
|
||||
}
|
||||
@@ -510,7 +523,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
std::fs::write(&dbfile, b"123").unwrap();
|
||||
let res = Context::new("FakeOs".into(), dbfile.into()).await;
|
||||
let res = Context::new("FakeOs".into(), dbfile.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -525,7 +538,9 @@ 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()).await.unwrap();
|
||||
Context::new("FakeOS".into(), dbfile.into(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
assert!(blobdir.is_dir());
|
||||
}
|
||||
@@ -536,7 +551,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()).await;
|
||||
let res = Context::new("FakeOS".into(), dbfile.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -546,7 +561,9 @@ mod tests {
|
||||
let subdir = tmp.path().join("subdir");
|
||||
let dbfile = subdir.join("db.sqlite");
|
||||
let dbfile2 = dbfile.clone();
|
||||
Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
|
||||
Context::new("FakeOS".into(), dbfile.into(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(subdir.is_dir());
|
||||
assert!(dbfile2.is_file());
|
||||
}
|
||||
@@ -556,7 +573,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.into()).await;
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -565,7 +582,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()).await;
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -12,14 +12,16 @@ use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
|
||||
use crate::error::{bail, ensure, format_err, Result};
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::job::{self, Action};
|
||||
use crate::message::{self, MessageState, MessengerMessage, MsgId};
|
||||
use crate::mimeparser::*;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
use crate::securejoin::{
|
||||
self, handle_securejoin_handshake, observe_securejoin_on_other_device, BobStatus,
|
||||
};
|
||||
use crate::stock::StockMessage;
|
||||
use crate::{contact, location};
|
||||
|
||||
@@ -93,8 +95,8 @@ pub async fn dc_receive_imf(
|
||||
if let Some(create_event_to_send) = create_event_to_send {
|
||||
for (chat_id, msg_id) in created_db_entries {
|
||||
let event = match create_event_to_send {
|
||||
CreateEvent::MsgsChanged => Event::MsgsChanged { msg_id, chat_id },
|
||||
CreateEvent::IncomingMsg => Event::IncomingMsg { msg_id, chat_id },
|
||||
CreateEvent::MsgsChanged => EventType::MsgsChanged { msg_id, chat_id },
|
||||
CreateEvent::IncomingMsg => EventType::IncomingMsg { msg_id, chat_id },
|
||||
};
|
||||
context.emit_event(event);
|
||||
}
|
||||
@@ -111,7 +113,7 @@ pub async fn dc_receive_imf(
|
||||
// or if From: is equal to SELF (in this case, it is any outgoing messages,
|
||||
// we do not check Return-Path any more as this is unreliable, see
|
||||
// https://github.com/deltachat/deltachat-core/issues/150)
|
||||
let (from_id, from_id_blocked, incoming_origin) =
|
||||
let (from_id, _from_id_blocked, incoming_origin) =
|
||||
from_field_to_contact_id(context, &mime_parser.from).await?;
|
||||
|
||||
let incoming = from_id != DC_CONTACT_ID_SELF;
|
||||
@@ -162,7 +164,6 @@ pub async fn dc_receive_imf(
|
||||
&rfc724_mid,
|
||||
&mut sent_timestamp,
|
||||
from_id,
|
||||
from_id_blocked,
|
||||
&mut hidden,
|
||||
&mut chat_id,
|
||||
seen,
|
||||
@@ -206,7 +207,7 @@ pub async fn dc_receive_imf(
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "reveive_imf cannot update profile image: {}", err);
|
||||
@@ -329,7 +330,6 @@ async fn add_parts(
|
||||
rfc724_mid: &str,
|
||||
sent_timestamp: &mut i64,
|
||||
from_id: u32,
|
||||
from_id_blocked: bool,
|
||||
hidden: &mut bool,
|
||||
chat_id: &mut ChatId,
|
||||
seen: bool,
|
||||
@@ -419,7 +419,7 @@ async fn add_parts(
|
||||
Err(err) => {
|
||||
*hidden = true;
|
||||
|
||||
context.bob.write().await.status = 0; // secure-join failed
|
||||
context.bob.write().await.status = BobStatus::Error; // secure-join failed
|
||||
context.stop_ongoing().await;
|
||||
warn!(context, "Error in Secure-Join message handling: {}", err);
|
||||
return Ok(());
|
||||
@@ -432,6 +432,8 @@ async fn add_parts(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
// get the chat_id - a chat_id here is no indicator that the chat is displayed in the normal list,
|
||||
// it might also be blocked and displayed in the deaddrop as a result
|
||||
if chat_id.is_unset() && mime_parser.failure_report.is_some() {
|
||||
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
|
||||
info!(
|
||||
@@ -440,11 +442,8 @@ async fn add_parts(
|
||||
);
|
||||
}
|
||||
|
||||
// get the chat_id - a chat_id here is no indicator that the chat is displayed in the normal list,
|
||||
// it might also be blocked and displayed in the deaddrop as a result
|
||||
if chat_id.is_unset() {
|
||||
// try to create a group
|
||||
// (groups appear automatically only if the _sender_ is known, see core issue #54)
|
||||
|
||||
let create_blocked =
|
||||
if !test_normal_chat_id.is_unset() && test_normal_chat_id_blocked == Blocked::Not {
|
||||
@@ -663,47 +662,58 @@ async fn add_parts(
|
||||
&& !is_mdn
|
||||
&& (*chat_id).get_ephemeral_timer(context).await? != ephemeral_timer
|
||||
{
|
||||
match (*chat_id)
|
||||
if let Err(err) = (*chat_id)
|
||||
.inner_set_ephemeral_timer(context, ephemeral_timer)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
|
||||
set_better_msg(
|
||||
mime_parser,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
);
|
||||
|
||||
// Do not delete the system message itself.
|
||||
//
|
||||
// This prevents confusion when timer is changed
|
||||
// to 1 week, and then changed to 1 hour: after 1
|
||||
// hour, only the message about the change to 1
|
||||
// week is left.
|
||||
ephemeral_timer = EphemeralTimer::Disabled;
|
||||
} else {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
*chat_id,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"failed to modify timer for chat {}: {}", chat_id, err
|
||||
);
|
||||
}
|
||||
warn!(
|
||||
context,
|
||||
"failed to modify timer for chat {}: {}", chat_id, err
|
||||
);
|
||||
} else if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
*chat_id,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
|
||||
set_better_msg(
|
||||
mime_parser,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
);
|
||||
|
||||
// Do not delete the system message itself.
|
||||
//
|
||||
// This prevents confusion when timer is changed
|
||||
// to 1 week, and then changed to 1 hour: after 1
|
||||
// hour, only the message about the change to 1
|
||||
// week is left.
|
||||
ephemeral_timer = EphemeralTimer::Disabled;
|
||||
}
|
||||
|
||||
// correct message_timestamp, it should not be used before,
|
||||
// however, we cannot do this earlier as we need from_id to be set
|
||||
let in_fresh = state == MessageState::InFresh;
|
||||
let rcvd_timestamp = time();
|
||||
let sort_timestamp = calc_sort_timestamp(context, *sent_timestamp, *chat_id, in_fresh).await;
|
||||
|
||||
// Ensure replies to messages are sorted after the parent message.
|
||||
//
|
||||
// This is useful in a case where sender clocks are not
|
||||
// synchronized and parent message has a Date: header with a
|
||||
// timestamp higher than reply timestamp.
|
||||
//
|
||||
// This does not help if parent message arrives later than the
|
||||
// reply.
|
||||
let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
|
||||
let sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
|
||||
std::cmp::max(sort_timestamp, parent_timestamp)
|
||||
});
|
||||
|
||||
*sent_timestamp = std::cmp::min(*sent_timestamp, rcvd_timestamp);
|
||||
|
||||
// unarchive chat
|
||||
@@ -843,9 +853,7 @@ async fn add_parts(
|
||||
if chat_id.is_trash() || *hidden {
|
||||
*create_event_to_send = None;
|
||||
} else if incoming && state == MessageState::InFresh {
|
||||
if from_id_blocked {
|
||||
*create_event_to_send = None;
|
||||
} else if Blocked::Not != chat_id_blocked {
|
||||
if Blocked::Not != chat_id_blocked {
|
||||
*create_event_to_send = Some(CreateEvent::MsgsChanged);
|
||||
} else {
|
||||
*create_event_to_send = Some(CreateEvent::IncomingMsg);
|
||||
@@ -938,7 +946,7 @@ async fn save_locations(
|
||||
}
|
||||
}
|
||||
if send_event {
|
||||
context.emit_event(Event::LocationChanged(Some(from_id)));
|
||||
context.emit_event(EventType::LocationChanged(Some(from_id)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1242,7 +1250,7 @@ async fn create_or_lookup_group(
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1301,7 +1309,7 @@ async fn create_or_lookup_group(
|
||||
}
|
||||
|
||||
if send_EVENT_CHAT_MODIFIED {
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
Ok((chat_id, chat_id_blocked))
|
||||
}
|
||||
@@ -1438,7 +1446,7 @@ async fn create_or_lookup_adhoc_group(
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, member_id).await;
|
||||
}
|
||||
|
||||
context.emit_event(Event::ChatModified(new_chat_id));
|
||||
context.emit_event(EventType::ChatModified(new_chat_id));
|
||||
|
||||
Ok((new_chat_id, create_blocked))
|
||||
}
|
||||
@@ -1618,7 +1626,7 @@ async fn check_verified_properties(
|
||||
// this check is skipped for SELF as there is no proper SELF-peerstate
|
||||
// and results in group-splits otherwise.
|
||||
if from_id != DC_CONTACT_ID_SELF {
|
||||
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await;
|
||||
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
|
||||
if peerstate.is_none()
|
||||
|| contact.is_verified_ex(context, peerstate.as_ref()).await
|
||||
@@ -1672,7 +1680,7 @@ async fn check_verified_properties(
|
||||
context.is_self_addr(&to_addr).await
|
||||
);
|
||||
let mut is_verified = _is_verified != 0;
|
||||
let peerstate = Peerstate::from_addr(context, &to_addr).await;
|
||||
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
|
||||
|
||||
// mark gossiped keys (if any) as verified
|
||||
if mimeparser.gossipped_addr.contains(&to_addr) {
|
||||
@@ -2475,7 +2483,7 @@ mod tests {
|
||||
"haeclirth.sinoenrat@yahoo.com",
|
||||
"1680295672.3657931.1591783872936@mail.yahoo.com",
|
||||
include_bytes!("../test-data/message/yahoo_ndn.eml"),
|
||||
"Failure Notice – Sorry, we were unable to deliver your message to the following address.\n\n<haeclirth.sinoenrat@yahoo.com>:\n554: delivery error: dd Not a valid recipient - atlas117.free.mail.ne1.yahoo.com"
|
||||
"Failure Notice – Sorry, we were unable to deliver your message to the following address.\n\n<haeclirth.sinoenrat@yahoo.com>:\n554: delivery error: dd Not a valid recipient - atlas117.free.mail.ne1.yahoo.com [...]"
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -2499,7 +2507,7 @@ mod tests {
|
||||
"snaerituhaeirns@gmail.com",
|
||||
"9c9c2a32-056b-3592-c372-d7e8f0bd4bc2@gmx.de",
|
||||
include_bytes!("../test-data/message/gmx_ndn.eml"),
|
||||
"Mail delivery failed: returning message to sender – This message was created automatically by mail delivery software.\n\nA message that you sent could not be delivered to one or more of\nits recipients. This is a permanent error. The following address(es)\nfailed:\n\nsnaerituhaeirns@gmail.com:\nSMTP error from remote server for RCPT TO command, host: gmail-smtp-in.l.google.com (66.102.1.27) reason: 550-5.1.1 The email account that you tried to reach does not exist. Please\n try\n550-5.1.1 double-checking the recipient\'s email address for typos or\n550-5.1.1 unnecessary spaces. Learn more at\n550 5.1.1 https://support.google.com/mail/?p=NoSuchUser f6si2517766wmc.21\n9 - gsmtp"
|
||||
"Mail delivery failed: returning message to sender – This message was created automatically by mail delivery software.\n\nA message that you sent could not be delivered to one or more of\nits recipients. This is a permanent error. The following address(es)\nfailed:\n\nsnaerituhaeirns@gmail.com:\nSMTP error from remote server for RCPT TO command, host: gmail-smtp-in.l.google.com (66.102.1.27) reason: 550-5.1.1 The email account that you tried to reach does not exist. Please\n try\n550-5.1.1 double-checking the recipient\'s email address for typos or\n550-5.1.1 unnecessary spaces. Learn more at\n550 5.1.1 https://support.google.com/mail/?p=NoSuchUser f6si2517766wmc.21\n9 - gsmtp [...]"
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -2626,4 +2634,26 @@ mod tests {
|
||||
);
|
||||
assert_eq!(last_msg.from_id, DC_CONTACT_ID_INFO);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_html_only_mail() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.ctx
|
||||
.set_config(Config::ShowEmails, Some("2"))
|
||||
.await
|
||||
.unwrap();
|
||||
dc_receive_imf(
|
||||
&t.ctx,
|
||||
include_bytes!("../test-data/message/wrong-html.eml"),
|
||||
"INBOX",
|
||||
0,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
let msg_id = chats.get_msg_id(0).unwrap();
|
||||
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
|
||||
assert_eq!(msg.text.unwrap(), " Guten Abend, \n\n Lots of text \n\n text with Umlaut ä... \n\n MfG [...]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,15 @@ use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{fs, io};
|
||||
|
||||
use chrono::{Local, TimeZone};
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::{bail, Error};
|
||||
use crate::events::Event;
|
||||
|
||||
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
||||
0 != v && 0 == v & (v - 1)
|
||||
}
|
||||
use crate::events::EventType;
|
||||
|
||||
/// Shortens a string to a specified length and adds "[...]" to the
|
||||
/// end of the shortened string.
|
||||
@@ -243,7 +241,7 @@ pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
|
||||
|
||||
/// Returns the `(width, height)` of the given image buffer.
|
||||
pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
|
||||
let image = image::io::Reader::new(Cursor::new(buf));
|
||||
let image = image::io::Reader::new(Cursor::new(buf)).with_guessed_format()?;
|
||||
let dimensions = image.into_dimensions()?;
|
||||
Ok(dimensions)
|
||||
}
|
||||
@@ -286,7 +284,7 @@ pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) ->
|
||||
let dpath = format!("{}", path.as_ref().to_string_lossy());
|
||||
match fs::remove_file(path_abs).await {
|
||||
Ok(_) => {
|
||||
context.emit_event(Event::DeletedBlobFile(dpath));
|
||||
context.emit_event(EventType::DeletedBlobFile(dpath));
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -296,6 +294,23 @@ pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) ->
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dc_delete_files_in_dir(context: &Context, path: impl AsRef<Path>) {
|
||||
match async_std::fs::read_dir(path).await {
|
||||
Ok(mut read_dir) => {
|
||||
while let Some(entry) = read_dir.next().await {
|
||||
match entry {
|
||||
Ok(file) => {
|
||||
dc_delete_file(context, file.file_name()).await;
|
||||
}
|
||||
Err(e) => warn!(context, "Could not read file to delete: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => warn!(context, "Could not read dir to delete: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn dc_copy_file(
|
||||
context: &Context,
|
||||
src_path: impl AsRef<Path>,
|
||||
@@ -450,7 +465,7 @@ pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn dc_get_next_backup_path(
|
||||
pub(crate) async fn get_next_backup_path_old(
|
||||
folder: impl AsRef<Path>,
|
||||
backup_time: i64,
|
||||
) -> Result<PathBuf, Error> {
|
||||
@@ -470,6 +485,32 @@ pub(crate) async fn dc_get_next_backup_path(
|
||||
bail!("could not create backup file, disk full?");
|
||||
}
|
||||
|
||||
/// 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_new(
|
||||
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)
|
||||
.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)
|
||||
@@ -586,6 +627,16 @@ pub(crate) fn listflags_has(listflags: u32, bitindex: usize) -> bool {
|
||||
(listflags & bitindex) == bitindex
|
||||
}
|
||||
|
||||
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
|
||||
pub(crate) fn improve_single_line_input(input: impl AsRef<str>) -> String {
|
||||
input
|
||||
.as_ref()
|
||||
.replace("\n", " ")
|
||||
.replace("\r", " ")
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -916,4 +967,28 @@ mod tests {
|
||||
"3h 1m 0s"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_filemeta() {
|
||||
let data = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
let (w, h) = dc_get_filemeta(data).unwrap();
|
||||
assert_eq!(w, 900);
|
||||
assert_eq!(h, 900);
|
||||
|
||||
let data = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
let (w, h) = dc_get_filemeta(data).unwrap();
|
||||
assert_eq!(w, 1000);
|
||||
assert_eq!(h, 1000);
|
||||
|
||||
let data = include_bytes!("../test-data/image/image100x50.gif");
|
||||
let (w, h) = dc_get_filemeta(data).unwrap();
|
||||
assert_eq!(w, 100);
|
||||
assert_eq!(h, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_improve_single_line_input() {
|
||||
assert_eq!(improve_single_line_input("Hi\naiae "), "Hi aiae");
|
||||
assert_eq!(improve_single_line_input("\r\nahte\n\r"), "ahte");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,19 @@ enum AddText {
|
||||
// dehtml() returns way too many newlines; however, an optimisation on this issue is not needed as
|
||||
// the newlines are typically removed in further processing by the caller
|
||||
pub fn dehtml(buf: &str) -> String {
|
||||
let buf = buf.trim();
|
||||
let s = dehtml_quick_xml(buf);
|
||||
if !s.trim().is_empty() {
|
||||
return s;
|
||||
}
|
||||
let s = dehtml_manually(buf);
|
||||
if !s.trim().is_empty() {
|
||||
return s;
|
||||
}
|
||||
buf.to_string()
|
||||
}
|
||||
|
||||
pub fn dehtml_quick_xml(buf: &str) -> String {
|
||||
let buf = buf.trim().trim_start_matches("<!doctype html>");
|
||||
|
||||
let mut dehtml = Dehtml {
|
||||
strbuilder: String::with_capacity(buf.len()),
|
||||
@@ -46,6 +58,12 @@ pub fn dehtml(buf: &str) -> String {
|
||||
Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::CData(ref e)) => dehtml_cdata_cb(e, &mut dehtml),
|
||||
Ok(quick_xml::events::Event::Empty(ref e)) => {
|
||||
// Handle empty tags as a start tag immediately followed by end tag.
|
||||
// For example, `<p/>` is treated as `<p></p>`.
|
||||
dehtml_starttag_cb(e, &mut dehtml, &reader);
|
||||
dehtml_endtag_cb(&BytesEnd::borrowed(e.name()), &mut dehtml);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Parse html error: Error at position {}: {:?}",
|
||||
@@ -165,9 +183,28 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dehtml_manually(buf: &str) -> String {
|
||||
// Just strip out everything between "<" and ">"
|
||||
let mut strbuilder = String::new();
|
||||
let mut show_next_chars = true;
|
||||
for c in buf.chars() {
|
||||
match c {
|
||||
'<' => show_next_chars = false,
|
||||
'>' => show_next_chars = true,
|
||||
_ => {
|
||||
if show_next_chars {
|
||||
strbuilder.push(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
strbuilder
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::simplify::simplify;
|
||||
|
||||
#[test]
|
||||
fn test_dehtml() {
|
||||
@@ -176,25 +213,32 @@ mod tests {
|
||||
"<a href='https://example.com'> Foo </a>",
|
||||
"[ Foo ](https://example.com)",
|
||||
),
|
||||
("<img href='/foo.png'>", ""),
|
||||
("<b> bar </b>", "* bar *"),
|
||||
("<b> bar <i> foo", "* bar _ foo"),
|
||||
("& bar", "& bar"),
|
||||
// Note missing '
|
||||
("<a href='/foo.png>Hi</a> ", ""),
|
||||
// Despite missing ', this should be shown:
|
||||
("<a href='/foo.png>Hi</a> ", "Hi "),
|
||||
(
|
||||
"<a href='https://get.delta.chat/'/>",
|
||||
"[](https://get.delta.chat/)",
|
||||
),
|
||||
("", ""),
|
||||
("<!doctype html>\n<b>fat text</b>", "*fat text*"),
|
||||
// Invalid html (at least DC should show the text if the html is invalid):
|
||||
("<!some invalid html code>\n<b>some text</b>", "some text"),
|
||||
("<This text is in brackets>", "<This text is in brackets>"),
|
||||
];
|
||||
for (input, output) in cases {
|
||||
assert_eq!(dehtml(input), output);
|
||||
assert_eq!(simplify(dehtml(input), true).0, output);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dehtml_parse_br() {
|
||||
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
|
||||
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
|
||||
let plain = dehtml(html);
|
||||
|
||||
assert_eq!(plain, "line1\n\r\r\rline2");
|
||||
assert_eq!(plain, "line1\n\r\r\rline2\nline3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
14
src/e2ee.rs
@@ -15,7 +15,6 @@ use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::pgp;
|
||||
use crate::securejoin::handle_degrade_event;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EncryptHelper {
|
||||
@@ -140,7 +139,7 @@ pub async fn try_decrypt(
|
||||
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
|
||||
|
||||
if message_time > 0 {
|
||||
peerstate = Peerstate::from_addr(context, &from).await;
|
||||
peerstate = Peerstate::from_addr(context, &from).await?;
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
if let Some(ref header) = autocryptheader {
|
||||
@@ -163,17 +162,14 @@ pub async fn try_decrypt(
|
||||
let mut signatures = HashSet::default();
|
||||
|
||||
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
|
||||
peerstate = Peerstate::from_addr(&context, &from).await;
|
||||
peerstate = Peerstate::from_addr(&context, &from).await?;
|
||||
}
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.degrade_event.is_some() {
|
||||
handle_degrade_event(context, &peerstate).await?;
|
||||
}
|
||||
if let Some(key) = peerstate.gossip_key {
|
||||
public_keyring_for_validate.add(key);
|
||||
}
|
||||
peerstate.handle_fingerprint_change(context).await?;
|
||||
if let Some(key) = peerstate.public_key {
|
||||
public_keyring_for_validate.add(key);
|
||||
} else if let Some(key) = peerstate.gossip_key {
|
||||
public_keyring_for_validate.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ use crate::constants::{
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::error::{ensure, Error};
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql;
|
||||
@@ -177,7 +177,7 @@ impl ChatId {
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_event(Event::ChatEphemeralTimerModified {
|
||||
context.emit_event(EventType::ChatEphemeralTimerModified {
|
||||
chat_id: self,
|
||||
timer,
|
||||
});
|
||||
@@ -379,7 +379,7 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
async_std::task::sleep(duration).await;
|
||||
emit_event!(
|
||||
context1,
|
||||
Event::MsgsChanged {
|
||||
EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0)
|
||||
}
|
||||
@@ -390,7 +390,7 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
// Emit event immediately
|
||||
emit_event!(
|
||||
context,
|
||||
Event::MsgsChanged {
|
||||
EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! # Events specification
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::sync::{channel, Receiver, Sender, TrySendError};
|
||||
use strum::EnumProperty;
|
||||
@@ -54,14 +56,32 @@ impl EventEmitter {
|
||||
async_std::task::block_on(self.recv())
|
||||
}
|
||||
|
||||
/// Blocking async recv of an event. Return `None` if the `Sender` has been droped.
|
||||
/// Async recv of an event. Return `None` if the `Sender` has been droped.
|
||||
pub async fn recv(&self) -> Option<Event> {
|
||||
// TODO: change once we can use async channels internally.
|
||||
self.0.recv().await.ok()
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> Result<Event, async_std::sync::TryRecvError> {
|
||||
self.0.try_recv()
|
||||
}
|
||||
}
|
||||
|
||||
impl Event {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Event {
|
||||
pub id: u32,
|
||||
pub typ: EventType,
|
||||
}
|
||||
|
||||
impl Deref for Event {
|
||||
type Target = EventType;
|
||||
|
||||
fn deref(&self) -> &EventType {
|
||||
&self.typ
|
||||
}
|
||||
}
|
||||
|
||||
impl EventType {
|
||||
/// Returns the corresponding Event id.
|
||||
pub fn as_id(&self) -> i32 {
|
||||
self.get_str("id")
|
||||
@@ -72,7 +92,7 @@ impl Event {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty)]
|
||||
pub enum Event {
|
||||
pub enum EventType {
|
||||
/// The library-user may write an informational string to the log.
|
||||
/// Passed to the callback given to dc_context_new().
|
||||
/// This event should not be reported to the end-user using a popup or something like that.
|
||||
@@ -107,7 +127,7 @@ pub enum Event {
|
||||
#[strum(props(id = "150"))]
|
||||
NewBlobFile(String),
|
||||
|
||||
/// Emitted when an new file in the $BLOBDIR was created
|
||||
/// Emitted when an file in the $BLOBDIR was deleted
|
||||
#[strum(props(id = "151"))]
|
||||
DeletedBlobFile(String),
|
||||
|
||||
|
||||
136
src/format_flowed.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
///! # format=flowed support
|
||||
///!
|
||||
///! Format=flowed is defined in
|
||||
///! [RFC 3676](https://tools.ietf.org/html/rfc3676).
|
||||
///!
|
||||
///! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used
|
||||
///! during formatting, i.e., DelSp parameter introduced in RFC 3676
|
||||
///! is assumed to be set to "no".
|
||||
///!
|
||||
///! For received messages, DelSp parameter is honoured.
|
||||
|
||||
/// Wraps line to 72 characters using format=flowed soft breaks.
|
||||
///
|
||||
/// 72 characters is the limit recommended by RFC 3676.
|
||||
///
|
||||
/// The function breaks line only after SP and before non-whitespace
|
||||
/// characters. It also does not insert breaks before ">" to avoid the
|
||||
/// need to do space stuffing (see RFC 3676) for quotes.
|
||||
///
|
||||
/// If there are long words, line may still exceed the limits on line
|
||||
/// length. However, this should be rare and should not result in
|
||||
/// immediate mail rejection: SMTP (RFC 2821) limit is 998 characters,
|
||||
/// and Spam Assassin limit is 78 characters.
|
||||
fn format_line_flowed(line: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut buffer = String::new();
|
||||
let mut after_space = false;
|
||||
|
||||
for c in line.chars() {
|
||||
if c == ' ' {
|
||||
buffer.push(c);
|
||||
after_space = true;
|
||||
} else {
|
||||
if after_space && buffer.len() >= 72 && !c.is_whitespace() && c != '>' {
|
||||
// Flush the buffer and insert soft break (SP CRLF).
|
||||
result += &buffer;
|
||||
result += "\r\n";
|
||||
buffer = String::new();
|
||||
}
|
||||
buffer.push(c);
|
||||
after_space = false;
|
||||
}
|
||||
}
|
||||
result + &buffer
|
||||
}
|
||||
|
||||
/// Returns text formatted according to RFC 3767 (format=flowed).
|
||||
///
|
||||
/// This function accepts text separated by LF, but returns text
|
||||
/// separated by CRLF.
|
||||
///
|
||||
/// RFC 2646 technique is used to insert soft line breaks, so DelSp
|
||||
/// SHOULD be set to "no" when sending.
|
||||
pub fn format_flowed(text: &str) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
for line in text.split('\n') {
|
||||
if !result.is_empty() {
|
||||
result += "\r\n";
|
||||
}
|
||||
let line = line.trim_end();
|
||||
if line.len() > 78 {
|
||||
result += &format_line_flowed(line);
|
||||
} else {
|
||||
result += line;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Joins lines in format=flowed text.
|
||||
///
|
||||
/// Lines must be separated by single LF.
|
||||
///
|
||||
/// Quote processing is not supported, it is assumed that they are
|
||||
/// deleted during simplification.
|
||||
///
|
||||
/// Signature separator line is not processed here, it is assumed to
|
||||
/// be stripped beforehand.
|
||||
pub fn unformat_flowed(text: &str, delsp: bool) -> String {
|
||||
let mut result = String::new();
|
||||
let mut skip_newline = true;
|
||||
|
||||
for line in text.split('\n') {
|
||||
// Revert space-stuffing
|
||||
let line = line.strip_prefix(" ").unwrap_or(line);
|
||||
|
||||
if !skip_newline {
|
||||
result.push('\n');
|
||||
}
|
||||
|
||||
if let Some(line) = line.strip_suffix(" ") {
|
||||
// Flowed line
|
||||
result += line;
|
||||
if !delsp {
|
||||
result.push(' ');
|
||||
}
|
||||
skip_newline = true;
|
||||
} else {
|
||||
// Fixed line
|
||||
result += line;
|
||||
skip_newline = false;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_flowed() {
|
||||
let text = "Foo bar baz";
|
||||
assert_eq!(format_flowed(text), "Foo bar baz");
|
||||
|
||||
let text = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\
|
||||
\n\
|
||||
To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
|
||||
let expected = "This is the Autocrypt Setup Message used to transfer your key between clients.\r\n\
|
||||
\r\n\
|
||||
To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
|
||||
client and enter the setup code presented on the generating device.";
|
||||
assert_eq!(format_flowed(text), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unformat_flowed() {
|
||||
let text = "this is a very long message that should be wrapped using format=flowed and \n\
|
||||
unwrapped on the receiver";
|
||||
let expected =
|
||||
"this is a very long message that should be wrapped using format=flowed and \
|
||||
unwrapped on the receiver";
|
||||
assert_eq!(unformat_flowed(text, false), expected);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ impl Client {
|
||||
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
|
||||
self,
|
||||
auth_type: S,
|
||||
authenticator: &A,
|
||||
authenticator: A,
|
||||
) -> std::result::Result<Session, (ImapError, Self)> {
|
||||
let Client { inner, is_secure } = self;
|
||||
let session =
|
||||
@@ -85,9 +85,6 @@ impl Client {
|
||||
let tls_stream: Box<dyn SessionStream> =
|
||||
Box::new(tls.connect(domain.as_ref(), stream).await?);
|
||||
let mut client = ImapClient::new(tls_stream);
|
||||
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
|
||||
client.debug = true;
|
||||
}
|
||||
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
@@ -104,9 +101,6 @@ impl Client {
|
||||
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
|
||||
|
||||
let mut client = ImapClient::new(stream);
|
||||
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
|
||||
client.debug = true;
|
||||
}
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
|
||||
@@ -5,31 +5,11 @@ use async_imap::types::UnsolicitedResponse;
|
||||
use async_std::prelude::*;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use crate::error::{bail, format_err, Result};
|
||||
use crate::{context::Context, scheduler::InterruptInfo};
|
||||
|
||||
use super::select_folder;
|
||||
use super::session::Session;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("IMAP IDLE protocol failed to init/complete: {0}")]
|
||||
IdleProtocolFailed(#[from] async_imap::error::Error),
|
||||
|
||||
#[error("IMAP IDLE protocol timed out: {0}")]
|
||||
IdleTimeout(#[from] async_std::future::TimeoutError),
|
||||
|
||||
#[error("IMAP server does not have IDLE capability")]
|
||||
IdleAbilityMissing,
|
||||
|
||||
#[error("IMAP select folder error: {0}")]
|
||||
SelectFolderError(#[from] select_folder::Error),
|
||||
|
||||
#[error("Setup handle error: {0}")]
|
||||
SetupHandleError(#[from] super::Error),
|
||||
}
|
||||
|
||||
impl Imap {
|
||||
pub fn can_idle(&self) -> bool {
|
||||
self.config.can_idle
|
||||
@@ -43,7 +23,7 @@ impl Imap {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
if !self.can_idle() {
|
||||
return Err(Error::IdleAbilityMissing);
|
||||
bail!("IMAP server does not have IDLE capability");
|
||||
}
|
||||
self.setup_handle_if_needed(context).await?;
|
||||
|
||||
@@ -72,7 +52,7 @@ impl Imap {
|
||||
|
||||
let mut handle = session.idle();
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
|
||||
}
|
||||
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
@@ -115,7 +95,7 @@ impl Imap {
|
||||
.done()
|
||||
.timeout(Duration::from_secs(15))
|
||||
.await
|
||||
.map_err(Error::IdleTimeout)??;
|
||||
.map_err(|err| format_err!("IMAP IDLE protocol timed out: {}", err))??;
|
||||
self.session = Some(Session { inner: session });
|
||||
} else {
|
||||
warn!(context, "Attempted to idle without a session");
|
||||
|
||||
398
src/imap/mod.rs
@@ -19,70 +19,30 @@ use crate::context::Context;
|
||||
use crate::dc_receive_imf::{
|
||||
dc_receive_imf, from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list,
|
||||
};
|
||||
use crate::events::Event;
|
||||
use crate::error::{bail, format_err, Result};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::job::{self, Action};
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
|
||||
use crate::message::{self, update_server_uid, MessageState};
|
||||
use crate::mimeparser;
|
||||
use crate::oauth2::dc_get_oauth2_access_token;
|
||||
use crate::param::Params;
|
||||
use crate::provider::get_provider_info;
|
||||
use crate::{chat, scheduler::InterruptInfo, stock::StockMessage};
|
||||
use crate::provider::{get_provider_info, Socket};
|
||||
use crate::{
|
||||
chat, dc_tools::dc_extract_grpid_from_rfc724_mid, scheduler::InterruptInfo, stock::StockMessage,
|
||||
};
|
||||
|
||||
mod client;
|
||||
mod idle;
|
||||
pub mod select_folder;
|
||||
mod session;
|
||||
|
||||
use chat::get_chat_id_by_grpid;
|
||||
use client::Client;
|
||||
use message::Message;
|
||||
use session::Session;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("IMAP Connect without configured params")]
|
||||
ConnectWithoutConfigure,
|
||||
|
||||
#[error("IMAP Connection Failed params: {0}")]
|
||||
ConnectionFailed(String),
|
||||
|
||||
#[error("IMAP No Connection established")]
|
||||
NoConnection,
|
||||
|
||||
#[error("IMAP Could not get OAUTH token")]
|
||||
OauthError,
|
||||
|
||||
#[error("IMAP Could not login as {0}")]
|
||||
LoginFailed(String),
|
||||
|
||||
#[error("IMAP Could not fetch")]
|
||||
FetchFailed(#[from] async_imap::error::Error),
|
||||
|
||||
#[error("IMAP operation attempted while it is torn down")]
|
||||
InTeardown,
|
||||
|
||||
#[error("IMAP operation attempted while it is torn down")]
|
||||
SqlError(#[from] crate::sql::Error),
|
||||
|
||||
#[error("IMAP got error from elsewhere")]
|
||||
WrappedError(#[from] crate::error::Error),
|
||||
|
||||
#[error("IMAP select folder error")]
|
||||
SelectFolderError(#[from] select_folder::Error),
|
||||
|
||||
#[error("Mail parse error")]
|
||||
MailParseError(#[from] mailparse::MailParseError),
|
||||
|
||||
#[error("No mailbox selected, folder: {0}")]
|
||||
NoMailbox(String),
|
||||
|
||||
#[error("IMAP other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImapActionResult {
|
||||
Failed,
|
||||
@@ -105,6 +65,7 @@ const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
|
||||
AUTOCRYPT-SETUP-MESSAGE\
|
||||
)])";
|
||||
const DELETE_CHECK_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
|
||||
const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
|
||||
const JUST_UID: &str = "(UID)";
|
||||
const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])";
|
||||
const SELECT_ALL: &str = "1:*";
|
||||
@@ -129,7 +90,7 @@ struct OAuth2 {
|
||||
impl async_imap::Authenticator for OAuth2 {
|
||||
type Response = String;
|
||||
|
||||
fn process(&self, _data: &[u8]) -> Self::Response {
|
||||
fn process(&mut self, _data: &[u8]) -> Self::Response {
|
||||
format!(
|
||||
"user={}\x01auth=Bearer {}\x01\x01",
|
||||
self.user, self.access_token
|
||||
@@ -147,12 +108,9 @@ enum FolderMeaning {
|
||||
#[derive(Debug)]
|
||||
struct ImapConfig {
|
||||
pub addr: String,
|
||||
pub imap_server: String,
|
||||
pub imap_port: u16,
|
||||
pub imap_user: String,
|
||||
pub imap_pw: String,
|
||||
pub lp: ServerLoginParam,
|
||||
pub strict_tls: bool,
|
||||
pub server_flags: usize,
|
||||
pub oauth2: bool,
|
||||
pub selected_folder: Option<String>,
|
||||
pub selected_mailbox: Option<Mailbox>,
|
||||
pub selected_folder_needs_expunge: bool,
|
||||
@@ -167,12 +125,9 @@ impl Default for ImapConfig {
|
||||
fn default() -> Self {
|
||||
ImapConfig {
|
||||
addr: "".into(),
|
||||
imap_server: "".into(),
|
||||
imap_port: 0,
|
||||
imap_user: "".into(),
|
||||
imap_pw: "".into(),
|
||||
lp: Default::default(),
|
||||
strict_tls: false,
|
||||
server_flags: 0,
|
||||
oauth2: false,
|
||||
selected_folder: None,
|
||||
selected_mailbox: None,
|
||||
selected_folder_needs_expunge: false,
|
||||
@@ -208,8 +163,8 @@ impl Imap {
|
||||
}
|
||||
|
||||
async fn setup_handle_if_needed(&mut self, context: &Context) -> Result<()> {
|
||||
if self.config.imap_server.is_empty() {
|
||||
return Err(Error::InTeardown);
|
||||
if self.config.lp.server.is_empty() {
|
||||
bail!("IMAP operation attempted while it is torn down");
|
||||
}
|
||||
|
||||
if self.should_reconnect() {
|
||||
@@ -219,40 +174,40 @@ impl Imap {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let server_flags = self.config.server_flags as i32;
|
||||
let oauth2 = self.config.oauth2;
|
||||
|
||||
let connection_res: ImapResult<Client> =
|
||||
if (server_flags & (DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_PLAIN)) != 0 {
|
||||
let config = &mut self.config;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
let connection_res: ImapResult<Client> = if self.config.lp.security == Socket::STARTTLS
|
||||
|| self.config.lp.security == Socket::Plain
|
||||
{
|
||||
let config = &mut self.config;
|
||||
let imap_server: &str = config.lp.server.as_ref();
|
||||
let imap_port = config.lp.port;
|
||||
|
||||
match Client::connect_insecure((imap_server, imap_port)).await {
|
||||
Ok(client) => {
|
||||
if (server_flags & DC_LP_IMAP_SOCKET_STARTTLS) != 0 {
|
||||
client.secure(imap_server, config.strict_tls).await
|
||||
} else {
|
||||
Ok(client)
|
||||
}
|
||||
match Client::connect_insecure((imap_server, imap_port)).await {
|
||||
Ok(client) => {
|
||||
if config.lp.security == Socket::STARTTLS {
|
||||
client.secure(imap_server, config.strict_tls).await
|
||||
} else {
|
||||
Ok(client)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
} else {
|
||||
let config = &self.config;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
} else {
|
||||
let config = &self.config;
|
||||
let imap_server: &str = config.lp.server.as_ref();
|
||||
let imap_port = config.lp.port;
|
||||
|
||||
Client::connect_secure((imap_server, imap_port), imap_server, config.strict_tls)
|
||||
.await
|
||||
};
|
||||
Client::connect_secure((imap_server, imap_port), imap_server, config.strict_tls).await
|
||||
};
|
||||
|
||||
let login_res = match connection_res {
|
||||
Ok(client) => {
|
||||
let config = &self.config;
|
||||
let imap_user: &str = config.imap_user.as_ref();
|
||||
let imap_pw: &str = config.imap_pw.as_ref();
|
||||
let imap_user: &str = config.lp.user.as_ref();
|
||||
let imap_pw: &str = config.lp.password.as_ref();
|
||||
|
||||
if (server_flags & DC_LP_AUTH_OAUTH2) != 0 {
|
||||
if oauth2 {
|
||||
let addr: &str = config.addr.as_ref();
|
||||
|
||||
if let Some(token) =
|
||||
@@ -262,9 +217,9 @@ impl Imap {
|
||||
user: imap_user.into(),
|
||||
access_token: token,
|
||||
};
|
||||
client.authenticate("XOAUTH2", &auth).await
|
||||
client.authenticate("XOAUTH2", auth).await
|
||||
} else {
|
||||
return Err(Error::OauthError);
|
||||
bail!("IMAP Could not get OAUTH token");
|
||||
}
|
||||
} else {
|
||||
client.login(imap_user, imap_pw).await
|
||||
@@ -273,8 +228,8 @@ impl Imap {
|
||||
Err(err) => {
|
||||
let message = {
|
||||
let config = &self.config;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
let imap_server: &str = config.lp.server.as_ref();
|
||||
let imap_port = config.lp.port;
|
||||
context
|
||||
.stock_string_repl_str2(
|
||||
StockMessage::ServerResponse,
|
||||
@@ -284,8 +239,8 @@ impl Imap {
|
||||
.await
|
||||
};
|
||||
// IMAP connection failures are reported to users
|
||||
emit_event!(context, Event::ErrorNetwork(message));
|
||||
return Err(Error::ConnectionFailed(err.to_string()));
|
||||
emit_event!(context, EventType::ErrorNetwork(message));
|
||||
bail!("IMAP connection failed: {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -301,13 +256,13 @@ impl Imap {
|
||||
}
|
||||
|
||||
Err((err, _)) => {
|
||||
let imap_user = self.config.imap_user.to_owned();
|
||||
let imap_user = self.config.lp.user.to_owned();
|
||||
let message = context
|
||||
.stock_string_repl_str(StockMessage::CannotLogin, &imap_user)
|
||||
.await;
|
||||
|
||||
warn!(context, "{} ({})", message, err);
|
||||
emit_event!(context, Event::ErrorNetwork(message.clone()));
|
||||
emit_event!(context, EventType::ErrorNetwork(message.clone()));
|
||||
|
||||
let lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if self.login_failed_once
|
||||
@@ -331,7 +286,7 @@ impl Imap {
|
||||
}
|
||||
|
||||
self.trigger_reconnect();
|
||||
Err(Error::LoginFailed(format!("cannot login as {}", imap_user)))
|
||||
Err(format_err!("IMAP Could not login as {}", imap_user))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,10 +312,7 @@ impl Imap {
|
||||
let mut cfg = &mut self.config;
|
||||
|
||||
cfg.addr = "".into();
|
||||
cfg.imap_server = "".into();
|
||||
cfg.imap_user = "".into();
|
||||
cfg.imap_pw = "".into();
|
||||
cfg.imap_port = 0;
|
||||
cfg.lp = Default::default();
|
||||
|
||||
cfg.can_idle = false;
|
||||
cfg.can_move = false;
|
||||
@@ -372,41 +324,47 @@ impl Imap {
|
||||
return Ok(());
|
||||
}
|
||||
if !context.is_configured().await {
|
||||
return Err(Error::ConnectWithoutConfigure);
|
||||
bail!("IMAP Connect without configured params");
|
||||
}
|
||||
|
||||
let param = LoginParam::from_database(context, "configured_").await;
|
||||
// the trailing underscore is correct
|
||||
|
||||
if self.connect(context, ¶m).await {
|
||||
self.ensure_configured_folders(context, true).await
|
||||
if let Err(err) = self
|
||||
.connect(
|
||||
context,
|
||||
¶m.imap,
|
||||
¶m.addr,
|
||||
param.server_flags & DC_LP_AUTH_OAUTH2 != 0,
|
||||
)
|
||||
.await
|
||||
{
|
||||
bail!("IMAP Connection Failed with params {}: {}", param, err);
|
||||
} else {
|
||||
Err(Error::ConnectionFailed(format!("{}", param)))
|
||||
self.ensure_configured_folders(context, true).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries connecting to imap account using the specific login parameters.
|
||||
pub async fn connect(&mut self, context: &Context, lp: &LoginParam) -> bool {
|
||||
if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() {
|
||||
return false;
|
||||
///
|
||||
/// `addr` is used to renew token if OAuth2 authentication is used.
|
||||
pub async fn connect(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
lp: &ServerLoginParam,
|
||||
addr: &str,
|
||||
oauth2: bool,
|
||||
) -> Result<()> {
|
||||
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
|
||||
bail!("Incomplete IMAP connection parameters");
|
||||
}
|
||||
|
||||
{
|
||||
let addr = &lp.addr;
|
||||
let imap_server = &lp.mail_server;
|
||||
let imap_port = lp.mail_port as u16;
|
||||
let imap_user = &lp.mail_user;
|
||||
let imap_pw = &lp.mail_pw;
|
||||
let server_flags = lp.server_flags as usize;
|
||||
|
||||
let mut config = &mut self.config;
|
||||
config.addr = addr.to_string();
|
||||
config.imap_server = imap_server.to_string();
|
||||
config.imap_port = imap_port;
|
||||
config.imap_user = imap_user.to_string();
|
||||
config.imap_pw = imap_pw.to_string();
|
||||
let provider = get_provider_info(&lp.addr);
|
||||
config.strict_tls = match lp.imap_certificate_checks {
|
||||
config.lp = lp.clone();
|
||||
let provider = get_provider_info(&addr);
|
||||
config.strict_tls = match lp.certificate_checks {
|
||||
CertificateChecks::Automatic => {
|
||||
provider.map_or(false, |provider| provider.strict_tls)
|
||||
}
|
||||
@@ -414,20 +372,20 @@ impl Imap {
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => false,
|
||||
};
|
||||
config.server_flags = server_flags;
|
||||
config.oauth2 = oauth2;
|
||||
}
|
||||
|
||||
if let Err(err) = self.setup_handle_if_needed(context).await {
|
||||
warn!(context, "failed to setup imap handle: {}", err);
|
||||
self.free_connect_params().await;
|
||||
return false;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let teardown = match &mut self.session {
|
||||
Some(ref mut session) => match session.capabilities().await {
|
||||
Ok(caps) => {
|
||||
if !context.sql.is_open().await {
|
||||
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.mail_user,);
|
||||
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.user,);
|
||||
true
|
||||
} else {
|
||||
let can_idle = caps.has_str("IDLE");
|
||||
@@ -445,9 +403,9 @@ impl Imap {
|
||||
self.connected = true;
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ImapConnected(format!(
|
||||
EventType::ImapConnected(format!(
|
||||
"IMAP-LOGIN as {}, capabilities: {}",
|
||||
lp.mail_user, caps_list,
|
||||
lp.user, caps_list,
|
||||
))
|
||||
);
|
||||
false
|
||||
@@ -464,10 +422,9 @@ impl Imap {
|
||||
if teardown {
|
||||
self.disconnect(context).await;
|
||||
|
||||
false
|
||||
} else {
|
||||
true
|
||||
bail!("IMAP disconnected immediately after connecting due to error");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn disconnect(&mut self, context: &Context) {
|
||||
@@ -478,7 +435,7 @@ impl Imap {
|
||||
pub async fn fetch(&mut self, context: &Context, watch_folder: &str) -> Result<()> {
|
||||
if !context.sql.is_open().await {
|
||||
// probably shutdown
|
||||
return Err(Error::InTeardown);
|
||||
bail!("IMAP operation attempted while it is torn down");
|
||||
}
|
||||
self.setup_handle_if_needed(context).await?;
|
||||
|
||||
@@ -514,6 +471,81 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronizes UIDs in the database with UIDs on the server.
|
||||
///
|
||||
/// It is assumed that no operations are taking place on the same
|
||||
/// folder at the moment. Make sure to run it in the same
|
||||
/// thread/task as other network operations on this folder to
|
||||
/// avoid race conditions.
|
||||
pub(crate) async fn resync_folder_uids(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: String,
|
||||
) -> Result<()> {
|
||||
// Collect pairs of UID and Message-ID.
|
||||
let mut msg_ids = BTreeMap::new();
|
||||
|
||||
self.select_folder(context, Some(&folder)).await?;
|
||||
|
||||
let session = if let Some(ref mut session) = &mut self.session {
|
||||
session
|
||||
} else {
|
||||
bail!("IMAP No Connection established");
|
||||
};
|
||||
|
||||
match session.uid_fetch("1:*", RFC724MID_UID).await {
|
||||
Ok(mut list) => {
|
||||
while let Some(fetch) = list.next().await {
|
||||
let msg = fetch?;
|
||||
|
||||
// Get Message-ID
|
||||
let message_id = get_fetch_headers(&msg)
|
||||
.and_then(|headers| prefetch_get_message_id(&headers))
|
||||
.ok();
|
||||
|
||||
if let (Some(uid), Some(rfc724_mid)) = (msg.uid, message_id) {
|
||||
msg_ids.insert(uid, rfc724_mid);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("Can't resync folder {}: {}", folder, err);
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Resync: collected {} message IDs in folder {}",
|
||||
msg_ids.len(),
|
||||
&folder
|
||||
);
|
||||
|
||||
// Write collected UIDs to SQLite database.
|
||||
context
|
||||
.sql
|
||||
.with_conn(move |mut conn| {
|
||||
let conn2 = &mut conn;
|
||||
let tx = conn2.transaction()?;
|
||||
tx.execute(
|
||||
"UPDATE msgs SET server_uid=0 WHERE server_folder=?",
|
||||
params![folder],
|
||||
)?;
|
||||
for (uid, rfc724_mid) in &msg_ids {
|
||||
// This may detect previously undetected moved
|
||||
// messages, so we update server_folder too.
|
||||
tx.execute(
|
||||
"UPDATE msgs \
|
||||
SET server_folder=?,server_uid=? WHERE rfc724_mid=?",
|
||||
params![folder, uid, rfc724_mid],
|
||||
)?;
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// return Result with (uid_validity, last_seen_uid) tuple.
|
||||
pub(crate) async fn select_with_uidvalidity(
|
||||
&mut self,
|
||||
@@ -529,13 +561,12 @@ impl Imap {
|
||||
let mailbox = config
|
||||
.selected_mailbox
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::NoMailbox(folder.to_string()))?;
|
||||
.ok_or_else(|| format_err!("No mailbox selected, folder: {}", folder))?;
|
||||
|
||||
let new_uid_validity = match mailbox.uid_validity {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
let s = format!("No UIDVALIDITY for folder {:?}", folder);
|
||||
return Err(Error::Other(s));
|
||||
bail!("No UIDVALIDITY for folder {:?}", folder);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -573,24 +604,33 @@ impl Imap {
|
||||
let set = format!("{}", mailbox.exists);
|
||||
match session.fetch(set, JUST_UID).await {
|
||||
Ok(mut list) => {
|
||||
if let Some(Ok(msg)) = list.next().await {
|
||||
msg.uid.unwrap_or_default()
|
||||
let mut new_last_seen_uid = None;
|
||||
while let Some(fetch) = list.next().await.transpose()? {
|
||||
if fetch.message == mailbox.exists && fetch.uid.is_some() {
|
||||
new_last_seen_uid = fetch.uid;
|
||||
}
|
||||
}
|
||||
if let Some(new_last_seen_uid) = new_last_seen_uid {
|
||||
new_last_seen_uid
|
||||
} else {
|
||||
return Err(Error::Other("failed to fetch".into()));
|
||||
bail!("failed to fetch");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(Error::FetchFailed(err));
|
||||
bail!("IMAP Could not fetch: {}", err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::NoConnection);
|
||||
bail!("IMAP No Connection established");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.set_config_last_seen_uid(context, &folder, new_uid_validity, new_last_seen_uid)
|
||||
.await;
|
||||
if uid_validity != 0 || last_seen_uid != 0 {
|
||||
job::schedule_resync(context).await;
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"uid/validity change: new {}/{} current {}/{}",
|
||||
@@ -687,7 +727,7 @@ impl Imap {
|
||||
uid: u32,
|
||||
) -> Result<BTreeMap<u32, async_imap::types::Fetch>> {
|
||||
if self.session.is_none() {
|
||||
return Err(Error::NoConnection);
|
||||
bail!("IMAP No Connection established");
|
||||
}
|
||||
|
||||
let session = self.session.as_mut().unwrap();
|
||||
@@ -698,11 +738,11 @@ impl Imap {
|
||||
let mut list = session
|
||||
.uid_fetch(set, PREFETCH_FLAGS)
|
||||
.await
|
||||
.map_err(Error::FetchFailed)?;
|
||||
.map_err(|err| format_err!("IMAP Could not fetch: {}", err))?;
|
||||
|
||||
let mut msgs = BTreeMap::new();
|
||||
while let Some(fetch) = list.next().await {
|
||||
let msg = fetch.map_err(|err| Error::Other(err.to_string()))?;
|
||||
let msg = fetch?;
|
||||
if let Some(msg_uid) = msg.uid {
|
||||
msgs.insert(msg_uid, msg);
|
||||
}
|
||||
@@ -882,7 +922,7 @@ impl Imap {
|
||||
Ok(_) => {
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ImapMessageMoved(format!(
|
||||
EventType::ImapMessageMoved(format!(
|
||||
"IMAP Message {} moved to {}",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
@@ -926,7 +966,7 @@ impl Imap {
|
||||
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ImapMessageMoved(format!(
|
||||
EventType::ImapMessageMoved(format!(
|
||||
"IMAP Message {} copied to {} (delete FAILED)",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
@@ -936,7 +976,7 @@ impl Imap {
|
||||
self.config.selected_folder_needs_expunge = true;
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ImapMessageMoved(format!(
|
||||
EventType::ImapMessageMoved(format!(
|
||||
"IMAP Message {} copied to {} (delete successfull)",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
@@ -970,7 +1010,11 @@ impl Imap {
|
||||
if let Some(ref mut session) = &mut self.session {
|
||||
let query = format!("+FLAGS ({})", flag);
|
||||
match session.uid_store(uid_set, &query).await {
|
||||
Ok(_) => {}
|
||||
Ok(mut responses) => {
|
||||
while let Some(_response) = responses.next().await {
|
||||
// Read all the responses
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
@@ -1073,8 +1117,35 @@ impl Imap {
|
||||
if let Some(ref mut session) = &mut self.session {
|
||||
match session.uid_fetch(set, DELETE_CHECK_FLAGS).await {
|
||||
Ok(mut msgs) => {
|
||||
let fetch = if let Some(Ok(fetch)) = msgs.next().await {
|
||||
fetch
|
||||
let mut remote_message_id = None;
|
||||
|
||||
while let Some(response) = msgs.next().await {
|
||||
match response {
|
||||
Ok(fetch) => {
|
||||
if fetch.uid == Some(uid) {
|
||||
remote_message_id = get_fetch_headers(&fetch)
|
||||
.and_then(|headers| prefetch_get_message_id(&headers))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "IMAP fetch error {}", err);
|
||||
return ImapActionResult::RetryLater;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(remote_message_id) = remote_message_id {
|
||||
if remote_message_id != message_id {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot delete on IMAP, {}: remote message-id '{}' != '{}'",
|
||||
display_imap_id,
|
||||
remote_message_id,
|
||||
message_id,
|
||||
);
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
@@ -1083,21 +1154,6 @@ impl Imap {
|
||||
message_id,
|
||||
);
|
||||
return ImapActionResult::AlreadyDone;
|
||||
};
|
||||
|
||||
let remote_message_id = get_fetch_headers(&fetch)
|
||||
.and_then(|headers| prefetch_get_message_id(&headers))
|
||||
.unwrap_or_default();
|
||||
|
||||
if remote_message_id != message_id {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot delete on IMAP, {}: remote message-id '{}' != '{}'",
|
||||
display_imap_id,
|
||||
remote_message_id,
|
||||
message_id,
|
||||
);
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1120,7 +1176,7 @@ impl Imap {
|
||||
} else {
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ImapMessageDeleted(format!(
|
||||
EventType::ImapMessageDeleted(format!(
|
||||
"IMAP Message {} marked as deleted [{}]",
|
||||
display_imap_id, message_id
|
||||
))
|
||||
@@ -1148,14 +1204,14 @@ impl Imap {
|
||||
|
||||
pub async fn configure_folders(&mut self, context: &Context, create_mvbox: bool) -> Result<()> {
|
||||
if !self.is_connected() {
|
||||
return Err(Error::NoConnection);
|
||||
bail!("IMAP No Connection established");
|
||||
}
|
||||
|
||||
if let Some(ref mut session) = &mut self.session {
|
||||
let mut folders = match session.list(Some(""), Some("*")).await {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
return Err(Error::Other(format!("list_folders failed {:?}", err)));
|
||||
bail!("list_folders failed: {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1166,7 +1222,7 @@ impl Imap {
|
||||
let mut fallback_folder = get_fallback_folder(&delimiter);
|
||||
|
||||
while let Some(folder) = folders.next().await {
|
||||
let folder = folder.map_err(|err| Error::Other(err.to_string()))?;
|
||||
let folder = folder?;
|
||||
info!(context, "Scanning folder: {:?}", folder);
|
||||
|
||||
// Update the delimiter iff there is a different one, but only once.
|
||||
@@ -1294,7 +1350,7 @@ impl Imap {
|
||||
self.config.selected_folder_needs_expunge = true;
|
||||
match self.select_folder::<String>(context, None).await {
|
||||
Ok(()) => {
|
||||
emit_event!(context, Event::ImapFolderEmptied(folder.to_string()));
|
||||
emit_event!(context, EventType::ImapFolderEmptied(folder.to_string()));
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "expunge failed {}: {:?}", folder, err);
|
||||
@@ -1451,7 +1507,7 @@ fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String>
|
||||
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId) {
|
||||
Ok(crate::mimeparser::parse_message_id(&message_id)?)
|
||||
} else {
|
||||
Err(Error::Other("prefetch: No message ID found".to_string()))
|
||||
bail!("prefetch: No message ID found");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1479,6 +1535,18 @@ pub(crate) async fn prefetch_should_download(
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
show_emails: ShowEmails,
|
||||
) -> Result<bool> {
|
||||
if let Some(rfc724_mid) = headers.get_header_value(HeaderDef::MessageId) {
|
||||
if let Some(group_id) = dc_extract_grpid_from_rfc724_mid(&rfc724_mid) {
|
||||
if let Ok((chat_id, _, _)) = get_chat_id_by_grpid(context, group_id).await {
|
||||
if !chat_id.is_unset() {
|
||||
// This might be a group command, like removing a group member.
|
||||
// We really need to fetch this to avoid inconsistent group state.
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
||||
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers).await;
|
||||
|
||||
|
||||
273
src/imex.rs
@@ -1,10 +1,16 @@
|
||||
//! # Import/export module
|
||||
|
||||
use std::any::Any;
|
||||
use std::cmp::{max, min};
|
||||
use std::{
|
||||
cmp::{max, min},
|
||||
ffi::OsStr,
|
||||
};
|
||||
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{
|
||||
fs::{self, File},
|
||||
prelude::*,
|
||||
};
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
@@ -16,7 +22,7 @@ use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee;
|
||||
use crate::error::*;
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -24,6 +30,11 @@ use crate::param::*;
|
||||
use crate::pgp;
|
||||
use crate::sql::{self, Sql};
|
||||
use crate::stock::StockMessage;
|
||||
use async_tar::Archive;
|
||||
|
||||
// Name of the database file in the backup.
|
||||
const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
|
||||
const BLOBS_BACKUP_NAME: &str = "blobs_backup";
|
||||
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(i32)]
|
||||
@@ -42,8 +53,8 @@ pub enum ImexMode {
|
||||
/// Export a backup to the directory given as `param1`.
|
||||
/// 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>.bak`, if more than one backup is create on a day,
|
||||
/// the format is `delta-chat.<day>-<number>.bak`
|
||||
/// 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
|
||||
@@ -71,20 +82,79 @@ pub async fn imex(
|
||||
what: ImexMode,
|
||||
param1: Option<impl AsRef<Path>>,
|
||||
) -> Result<()> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
let cancel = context.alloc_ongoing().await?;
|
||||
let res = imex_inner(context, what, param1)
|
||||
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
|
||||
.await;
|
||||
|
||||
let res = async {
|
||||
let success = imex_inner(context, what, param1).await;
|
||||
match success {
|
||||
Ok(()) => {
|
||||
info!(context, "IMEX successfully completed");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
cleanup_aborted_imex(context, what).await;
|
||||
error!(context, "{}", err);
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
bail!("IMEX FAILED to complete: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
.race(async {
|
||||
cancel.recv().await.ok();
|
||||
cleanup_aborted_imex(context, what).await;
|
||||
Err(format_err!("canceled"))
|
||||
})
|
||||
.await;
|
||||
|
||||
context.free_ongoing().await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn cleanup_aborted_imex(context: &Context, what: ImexMode) {
|
||||
if what == ImexMode::ImportBackup {
|
||||
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 {
|
||||
context.sql.open(context, context.get_dbfile(), false).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the filename of the backup found (otherwise an error)
|
||||
pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
|
||||
let dir_name = dir_name.as_ref();
|
||||
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;
|
||||
|
||||
while let Some(dirent) = dir_iter.next().await {
|
||||
if let Ok(dirent) = dirent {
|
||||
let path = dirent.path();
|
||||
let name = dirent.file_name();
|
||||
let name: String = name.to_string_lossy().into();
|
||||
if name.starts_with("delta-chat")
|
||||
&& name.ends_with(".tar")
|
||||
&& (newest_backup_name.is_empty() || name > newest_backup_name)
|
||||
{
|
||||
// We just use string comparison to determine which backup is newer.
|
||||
// This works fine because the filenames have the form ...delta-chat-backup-2020-07-24-00.tar
|
||||
newest_backup_path = Some(path);
|
||||
newest_backup_name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: impl AsRef<Path>) -> Result<String> {
|
||||
let dir_name = dir_name.as_ref();
|
||||
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
|
||||
let mut newest_backup_time = 0;
|
||||
@@ -369,11 +439,11 @@ async fn imex_inner(
|
||||
ensure!(param.is_some(), "No Import/export dir/file given.");
|
||||
|
||||
info!(context, "Import/export process started.");
|
||||
context.emit_event(Event::ImexProgress(10));
|
||||
context.emit_event(EventType::ImexProgress(10));
|
||||
|
||||
ensure!(context.sql.is_open().await, "Database not opened.");
|
||||
|
||||
let path = param.unwrap();
|
||||
let path = param.ok_or_else(|| format_err!("Imex: Param was None"))?;
|
||||
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
|
||||
// before we export anything, make sure the private key exists
|
||||
if e2ee::ensure_secret_key_exists(context).await.is_err() {
|
||||
@@ -383,28 +453,89 @@ async fn imex_inner(
|
||||
}
|
||||
}
|
||||
|
||||
let success = match what {
|
||||
match what {
|
||||
ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
|
||||
ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
|
||||
ImexMode::ExportBackup => export_backup(context, path).await,
|
||||
ImexMode::ImportBackup => import_backup(context, path).await,
|
||||
};
|
||||
|
||||
match success {
|
||||
Ok(()) => {
|
||||
info!(context, "IMEX successfully completed");
|
||||
context.emit_event(Event::ImexProgress(1000));
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
context.emit_event(Event::ImexProgress(0));
|
||||
bail!("IMEX FAILED to complete: {}", err);
|
||||
}
|
||||
// TODO In some months we can change the export_backup_old() call to export_backup() and delete export_backup_old().
|
||||
// (now is 07/2020)
|
||||
ImexMode::ExportBackup => export_backup_old(context, path).await,
|
||||
// import_backup() will call import_backup_old() if this is an old backup.
|
||||
ImexMode::ImportBackup => import_backup(context, path).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Import Backup
|
||||
async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
|
||||
if backup_to_import
|
||||
.as_ref()
|
||||
.to_string_lossy()
|
||||
.ends_with(".bak")
|
||||
{
|
||||
// Backwards compability
|
||||
return import_backup_old(context, backup_to_import).await;
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Import \"{}\" to \"{}\".",
|
||||
backup_to_import.as_ref().display(),
|
||||
context.get_dbfile().display()
|
||||
);
|
||||
|
||||
ensure!(
|
||||
!context.is_configured().await,
|
||||
"Cannot import backups to accounts in use."
|
||||
);
|
||||
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 archive = Archive::new(backup_file);
|
||||
let mut entries = archive.entries()?;
|
||||
while let Some(file) = entries.next().await {
|
||||
let f = &mut file?;
|
||||
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?;
|
||||
context.emit_event(EventType::ImexProgress(400)); // Just guess the progress, we at least have the dbfile by now
|
||||
} else {
|
||||
// async_tar will unpack to blobdir/BLOBS_BACKUP_NAME, so we move the file afterwards.
|
||||
f.unpack_in(context.get_blobdir()).await?;
|
||||
let from_path = context.get_blobdir().join(f.path()?);
|
||||
if from_path.is_file().await {
|
||||
if let Some(name) = from_path.file_name() {
|
||||
fs::rename(&from_path, context.get_blobdir().join(name)).await?;
|
||||
} else {
|
||||
warn!(context, "No file name");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensure!(
|
||||
context
|
||||
.sql
|
||||
.open(&context, &context.get_dbfile(), false)
|
||||
.await,
|
||||
"could not re-open db"
|
||||
);
|
||||
|
||||
delete_and_reset_all_device_msgs(&context).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
|
||||
info!(
|
||||
context,
|
||||
"Import \"{}\" to \"{}\".",
|
||||
@@ -487,7 +618,7 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
if permille > 990 {
|
||||
permille = 990
|
||||
}
|
||||
context.emit_event(Event::ImexProgress(permille));
|
||||
context.emit_event(EventType::ImexProgress(permille));
|
||||
if file_blob.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -512,14 +643,90 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
/*******************************************************************************
|
||||
* Export backup
|
||||
******************************************************************************/
|
||||
/* the FILE_PROGRESS macro calls the callback with the permille of files processed.
|
||||
The macro avoids weird values of 0% or 100% while still working. */
|
||||
#[allow(unused)]
|
||||
async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> 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_new(dir, now).await?;
|
||||
let _d = DeleteOnDrop(temp_path.clone());
|
||||
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int(context, "backup_time", now as i32)
|
||||
.await?;
|
||||
sql::housekeeping(context).await;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("VACUUM;", paramsv![])
|
||||
.await
|
||||
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e));
|
||||
|
||||
// we close the database during the export
|
||||
context.sql.close().await;
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Backup '{}' to '{}'.",
|
||||
context.get_dbfile().display(),
|
||||
dest_path.display(),
|
||||
);
|
||||
|
||||
let res = export_backup_inner(context, &temp_path).await;
|
||||
|
||||
// we re-open the database after export is finished
|
||||
context
|
||||
.sql
|
||||
.open(&context, &context.get_dbfile(), false)
|
||||
.await;
|
||||
|
||||
match &res {
|
||||
Ok(_) => {
|
||||
fs::rename(temp_path, &dest_path).await?;
|
||||
context.emit_event(EventType::ImexFileWritten(dest_path));
|
||||
}
|
||||
Err(e) => {
|
||||
error!(context, "backup failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
struct DeleteOnDrop(PathBuf);
|
||||
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 fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> 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)
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::ImexProgress(500));
|
||||
|
||||
builder
|
||||
.append_dir_all(BLOBS_BACKUP_NAME, context.get_blobdir())
|
||||
.await?;
|
||||
|
||||
builder.finish().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn export_backup_old(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
|
||||
// FIXME: we should write to a temporary file first and rename it on success. this would guarantee the backup is complete.
|
||||
// let dest_path_filename = dc_get_next_backup_file(context, dir, res);
|
||||
let now = time();
|
||||
let dest_path_filename = dc_get_next_backup_path(dir, now).await?;
|
||||
let dest_path_filename = get_next_backup_path_old(dir, now).await?;
|
||||
let dest_path_string = dest_path_filename.to_string_lossy().to_string();
|
||||
|
||||
sql::housekeeping(context).await;
|
||||
@@ -563,7 +770,7 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
dest_sql
|
||||
.set_raw_config_int(context, "backup_time", now as i32)
|
||||
.await?;
|
||||
context.emit_event(Event::ImexFileWritten(dest_path_filename));
|
||||
context.emit_event(EventType::ImexFileWritten(dest_path_filename));
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
@@ -602,7 +809,7 @@ async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
|
||||
}
|
||||
processed_files_cnt += 1;
|
||||
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
|
||||
context.emit_event(Event::ImexProgress(permille));
|
||||
context.emit_event(EventType::ImexProgress(permille));
|
||||
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
@@ -769,7 +976,7 @@ where
|
||||
if res.is_err() {
|
||||
error!(context, "Cannot write key to {}", file_name.display());
|
||||
} else {
|
||||
context.emit_event(Event::ImexFileWritten(file_name));
|
||||
context.emit_event(EventType::ImexFileWritten(file_name));
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
82
src/job.rs
@@ -23,10 +23,9 @@ use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::ephemeral::load_imap_deletion_msgid;
|
||||
use crate::error::{bail, ensure, format_err, Error, Result};
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::imap::*;
|
||||
use crate::location;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::MsgId;
|
||||
use crate::message::{self, Message, MessageState};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
@@ -102,6 +101,10 @@ pub enum Action {
|
||||
MoveMsg = 200,
|
||||
DeleteMsgOnImap = 210,
|
||||
|
||||
// UID synchronization is high-priority to make sure correct UIDs
|
||||
// are used by message moving/deletion.
|
||||
ResyncFolders = 300,
|
||||
|
||||
// Jobs in the SMTP-thread, range from DC_SMTP_THREAD..DC_SMTP_THREAD+999
|
||||
MaybeSendLocations = 5005, // low priority ...
|
||||
MaybeSendLocationsEnded = 5007,
|
||||
@@ -124,6 +127,7 @@ impl From<Action> for Thread {
|
||||
|
||||
Housekeeping => Thread::Imap,
|
||||
DeleteMsgOnImap => Thread::Imap,
|
||||
ResyncFolders => Thread::Imap,
|
||||
EmptyServer => Thread::Imap,
|
||||
MarkseenMsgOnImap => Thread::Imap,
|
||||
MoveMsg => Thread::Imap,
|
||||
@@ -338,12 +342,9 @@ impl Job {
|
||||
|
||||
pub(crate) async fn send_msg_to_smtp(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
|
||||
// SMTP server, if not yet done
|
||||
if !smtp.is_connected().await {
|
||||
let loginparam = LoginParam::from_database(context, "configured_").await;
|
||||
if let Err(err) = smtp.connect(context, &loginparam).await {
|
||||
warn!(context, "SMTP connection failure: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
if let Err(err) = smtp.connect_configured(context).await {
|
||||
warn!(context, "SMTP connection failure: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
let filename = job_try!(job_try!(self
|
||||
@@ -486,12 +487,9 @@ impl Job {
|
||||
let recipients = vec![recipient];
|
||||
|
||||
// connect to SMTP server, if not yet done
|
||||
if !smtp.is_connected().await {
|
||||
let loginparam = LoginParam::from_database(context, "configured_").await;
|
||||
if let Err(err) = smtp.connect(context, &loginparam).await {
|
||||
warn!(context, "SMTP connection failure: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
if let Err(err) = smtp.connect_configured(context).await {
|
||||
warn!(context, "SMTP connection failure: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
self.smtp_send(context, recipients, body, self.job_id, smtp, || {
|
||||
@@ -565,6 +563,11 @@ impl Job {
|
||||
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 */
|
||||
@@ -619,6 +622,44 @@ impl Job {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
if let Some(sentbox_folder) = &context.get_config(Config::ConfiguredSentboxFolder).await {
|
||||
job_try!(
|
||||
imap.resync_folder_uids(context, sentbox_folder.to_string())
|
||||
.await
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await {
|
||||
job_try!(
|
||||
imap.resync_folder_uids(context, inbox_folder.to_string())
|
||||
.await
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await {
|
||||
job_try!(
|
||||
imap.resync_folder_uids(context, mvbox_folder.to_string())
|
||||
.await
|
||||
);
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
async fn empty_server(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
@@ -737,7 +778,7 @@ async fn set_delivered(context: &Context, msg_id: MsgId) {
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
context.emit_event(Event::MsgDelivered { chat_id, msg_id });
|
||||
context.emit_event(EventType::MsgDelivered { chat_id, msg_id });
|
||||
}
|
||||
|
||||
/// Constructs a job for sending a message.
|
||||
@@ -986,6 +1027,7 @@ async fn perform_job_action(
|
||||
}
|
||||
Action::EmptyServer => job.empty_server(context, connection.inbox()).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::Housekeeping => {
|
||||
@@ -1019,6 +1061,15 @@ async fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn schedule_resync(context: &Context) {
|
||||
kill_action(context, Action::ResyncFolders).await;
|
||||
add(
|
||||
context,
|
||||
Job::new(Action::ResyncFolders, 0, Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Creates a job.
|
||||
pub fn create(action: Action, foreign_id: i32, param: Params, delay_seconds: i64) -> Result<Job> {
|
||||
ensure!(
|
||||
@@ -1043,6 +1094,7 @@ pub async fn add(context: &Context, job: Job) {
|
||||
Action::Housekeeping
|
||||
| Action::EmptyServer
|
||||
| Action::DeleteMsgOnImap
|
||||
| Action::ResyncFolders
|
||||
| Action::MarkseenMsgOnImap
|
||||
| Action::MoveMsg => {
|
||||
info!(context, "interrupt: imap");
|
||||
|
||||
@@ -54,6 +54,7 @@ pub mod imex;
|
||||
mod scheduler;
|
||||
#[macro_use]
|
||||
pub mod job;
|
||||
mod format_flowed;
|
||||
pub mod key;
|
||||
mod keyring;
|
||||
pub mod location;
|
||||
@@ -79,6 +80,8 @@ mod dehtml;
|
||||
pub mod dc_receive_imf;
|
||||
pub mod dc_tools;
|
||||
|
||||
pub mod accounts;
|
||||
|
||||
/// if set imap/incoming and smtp/outgoing MIME messages will be printed
|
||||
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::constants::*;
|
||||
use crate::context::*;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::{ensure, Error};
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::job::{self, Job};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -227,7 +227,7 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
|
||||
.await;
|
||||
chat::add_info_msg(context, chat_id, stock_str).await;
|
||||
}
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if 0 != seconds {
|
||||
schedule_maybe_send_locations(context, false).await;
|
||||
job::add(
|
||||
@@ -301,7 +301,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
||||
}
|
||||
}
|
||||
if continue_streaming {
|
||||
context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
|
||||
context.emit_event(EventType::LocationChanged(Some(DC_CONTACT_ID_SELF)));
|
||||
};
|
||||
schedule_maybe_send_locations(context, false).await;
|
||||
}
|
||||
@@ -381,7 +381,7 @@ pub async fn delete_all(context: &Context) -> Result<(), Error> {
|
||||
.sql
|
||||
.execute("DELETE FROM locations;", paramsv![])
|
||||
.await?;
|
||||
context.emit_event(Event::LocationChanged(None));
|
||||
context.emit_event(EventType::LocationChanged(None));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String
|
||||
<Document>\n\
|
||||
<Placemark>\
|
||||
<Timestamp><when>{}</when></Timestamp>\
|
||||
<Point><coordinates>{:.2},{:.2}</coordinates></Point>\
|
||||
<Point><coordinates>{},{}</coordinates></Point>\
|
||||
</Placemark>\n\
|
||||
</Document>\n\
|
||||
</kml>",
|
||||
@@ -715,7 +715,7 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0)
|
||||
.await;
|
||||
chat::add_info_msg(context, chat_id, stock_str).await;
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
job::Status::Finished(Ok(()))
|
||||
@@ -757,4 +757,22 @@ mod tests {
|
||||
assert!(locations_ref[1].accuracy < 2.6f64);
|
||||
assert_eq!(locations_ref[1].timestamp, 1544739072);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_message_kml() {
|
||||
let context = TestContext::new().await;
|
||||
let timestamp = 1598490000;
|
||||
|
||||
let xml = get_message_kml(timestamp, 51.423723f64, 8.552556f64);
|
||||
let kml = Kml::parse(&context.ctx, xml.as_bytes()).expect("parsing failed");
|
||||
let locations_ref = &kml.locations;
|
||||
assert_eq!(locations_ref.len(), 1);
|
||||
|
||||
assert!(locations_ref[0].latitude >= 51.423723f64);
|
||||
assert!(locations_ref[0].latitude < 51.423724f64);
|
||||
assert!(locations_ref[0].longitude >= 8.552556f64);
|
||||
assert!(locations_ref[0].longitude < 8.552557f64);
|
||||
assert_eq!(locations_ref[0].accuracy, 0.0f64);
|
||||
assert_eq!(locations_ref[0].timestamp, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ macro_rules! info {
|
||||
file = file!(),
|
||||
line = line!(),
|
||||
msg = &formatted);
|
||||
emit_event!($ctx, $crate::Event::Info(full));
|
||||
emit_event!($ctx, $crate::EventType::Info(full));
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ macro_rules! warn {
|
||||
file = file!(),
|
||||
line = line!(),
|
||||
msg = &formatted);
|
||||
emit_event!($ctx, $crate::Event::Warning(full));
|
||||
emit_event!($ctx, $crate::EventType::Warning(full));
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ macro_rules! error {
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
emit_event!($ctx, $crate::Event::Error(formatted));
|
||||
emit_event!($ctx, $crate::EventType::Error(formatted));
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ macro_rules! error_network {
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
emit_event!($ctx, $crate::Event::ErrorNetwork(formatted));
|
||||
emit_event!($ctx, $crate::EventType::ErrorNetwork(formatted));
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::{context::Context, provider::Socket};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive)]
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum CertificateChecks {
|
||||
/// Same as AcceptInvalidCertificates unless overridden by
|
||||
/// `strict_tls` setting in provider database.
|
||||
Automatic = 0,
|
||||
|
||||
Strict = 1,
|
||||
|
||||
/// Same as AcceptInvalidCertificates
|
||||
@@ -25,30 +28,29 @@ impl Default for CertificateChecks {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
/// Login parameters for a single server, either IMAP or SMTP
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct ServerLoginParam {
|
||||
pub server: String,
|
||||
pub user: String,
|
||||
pub password: String,
|
||||
pub port: u16,
|
||||
pub security: Socket,
|
||||
|
||||
/// TLS options: whether to allow invalid certificates and/or
|
||||
/// invalid hostnames
|
||||
pub certificate_checks: CertificateChecks,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct LoginParam {
|
||||
pub addr: String,
|
||||
pub mail_server: String,
|
||||
pub mail_user: String,
|
||||
pub mail_pw: String,
|
||||
pub mail_port: i32,
|
||||
/// IMAP TLS options: whether to allow invalid certificates and/or invalid hostnames
|
||||
pub imap_certificate_checks: CertificateChecks,
|
||||
pub send_server: String,
|
||||
pub send_user: String,
|
||||
pub send_pw: String,
|
||||
pub send_port: i32,
|
||||
/// SMTP TLS options: whether to allow invalid certificates and/or invalid hostnames
|
||||
pub smtp_certificate_checks: CertificateChecks,
|
||||
pub imap: ServerLoginParam,
|
||||
pub smtp: ServerLoginParam,
|
||||
pub server_flags: i32,
|
||||
}
|
||||
|
||||
impl LoginParam {
|
||||
/// Create a new `LoginParam` with default values.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Read the login parameters from the database.
|
||||
pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
|
||||
let prefix = prefix.as_ref();
|
||||
@@ -77,6 +79,13 @@ impl LoginParam {
|
||||
let key = format!("{}mail_pw", prefix);
|
||||
let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
|
||||
let key = format!("{}mail_security", prefix);
|
||||
let mail_security = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.and_then(num_traits::FromPrimitive::from_i32)
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = format!("{}imap_certificate_checks", prefix);
|
||||
let imap_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
|
||||
@@ -100,6 +109,13 @@ impl LoginParam {
|
||||
let key = format!("{}send_pw", prefix);
|
||||
let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
|
||||
|
||||
let key = format!("{}send_security", prefix);
|
||||
let send_security = sql
|
||||
.get_raw_config_int(context, key)
|
||||
.await
|
||||
.and_then(num_traits::FromPrimitive::from_i32)
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = format!("{}smtp_certificate_checks", prefix);
|
||||
let smtp_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
|
||||
@@ -116,16 +132,22 @@ impl LoginParam {
|
||||
|
||||
LoginParam {
|
||||
addr,
|
||||
mail_server,
|
||||
mail_user,
|
||||
mail_pw,
|
||||
mail_port,
|
||||
imap_certificate_checks,
|
||||
send_server,
|
||||
send_user,
|
||||
send_pw,
|
||||
send_port,
|
||||
smtp_certificate_checks,
|
||||
imap: ServerLoginParam {
|
||||
server: mail_server,
|
||||
user: mail_user,
|
||||
password: mail_pw,
|
||||
port: mail_port as u16,
|
||||
security: mail_security,
|
||||
certificate_checks: imap_certificate_checks,
|
||||
},
|
||||
smtp: ServerLoginParam {
|
||||
server: send_server,
|
||||
user: send_user,
|
||||
password: send_pw,
|
||||
port: send_port as u16,
|
||||
security: send_security,
|
||||
certificate_checks: smtp_certificate_checks,
|
||||
},
|
||||
server_flags,
|
||||
}
|
||||
}
|
||||
@@ -143,41 +165,51 @@ impl LoginParam {
|
||||
sql.set_raw_config(context, key, Some(&self.addr)).await?;
|
||||
|
||||
let key = format!("{}mail_server", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.mail_server))
|
||||
sql.set_raw_config(context, key, Some(&self.imap.server))
|
||||
.await?;
|
||||
|
||||
let key = format!("{}mail_port", prefix);
|
||||
sql.set_raw_config_int(context, key, self.mail_port).await?;
|
||||
sql.set_raw_config_int(context, key, self.imap.port as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}mail_user", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.mail_user))
|
||||
sql.set_raw_config(context, key, Some(&self.imap.user))
|
||||
.await?;
|
||||
|
||||
let key = format!("{}mail_pw", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.mail_pw))
|
||||
sql.set_raw_config(context, key, Some(&self.imap.password))
|
||||
.await?;
|
||||
|
||||
let key = format!("{}mail_security", prefix);
|
||||
sql.set_raw_config_int(context, key, self.imap.security as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}imap_certificate_checks", prefix);
|
||||
sql.set_raw_config_int(context, key, self.imap_certificate_checks as i32)
|
||||
sql.set_raw_config_int(context, key, self.imap.certificate_checks as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}send_server", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.send_server))
|
||||
sql.set_raw_config(context, key, Some(&self.smtp.server))
|
||||
.await?;
|
||||
|
||||
let key = format!("{}send_port", prefix);
|
||||
sql.set_raw_config_int(context, key, self.send_port).await?;
|
||||
sql.set_raw_config_int(context, key, self.smtp.port as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}send_user", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.send_user))
|
||||
sql.set_raw_config(context, key, Some(&self.smtp.user))
|
||||
.await?;
|
||||
|
||||
let key = format!("{}send_pw", prefix);
|
||||
sql.set_raw_config(context, key, Some(&self.send_pw))
|
||||
sql.set_raw_config(context, key, Some(&self.smtp.password))
|
||||
.await?;
|
||||
|
||||
let key = format!("{}send_security", prefix);
|
||||
sql.set_raw_config_int(context, key, self.smtp.security as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}smtp_certificate_checks", prefix);
|
||||
sql.set_raw_config_int(context, key, self.smtp_certificate_checks as i32)
|
||||
sql.set_raw_config_int(context, key, self.smtp.certificate_checks as i32)
|
||||
.await?;
|
||||
|
||||
let key = format!("{}server_flags", prefix);
|
||||
@@ -199,16 +231,24 @@ impl fmt::Display for LoginParam {
|
||||
f,
|
||||
"{} imap:{}:{}:{}:{}:cert_{} smtp:{}:{}:{}:{}:cert_{} {}",
|
||||
unset_empty(&self.addr),
|
||||
unset_empty(&self.mail_user),
|
||||
if !self.mail_pw.is_empty() { pw } else { unset },
|
||||
unset_empty(&self.mail_server),
|
||||
self.mail_port,
|
||||
self.imap_certificate_checks,
|
||||
unset_empty(&self.send_user),
|
||||
if !self.send_pw.is_empty() { pw } else { unset },
|
||||
unset_empty(&self.send_server),
|
||||
self.send_port,
|
||||
self.smtp_certificate_checks,
|
||||
unset_empty(&self.imap.user),
|
||||
if !self.imap.password.is_empty() {
|
||||
pw
|
||||
} else {
|
||||
unset
|
||||
},
|
||||
unset_empty(&self.imap.server),
|
||||
self.imap.port,
|
||||
self.imap.certificate_checks,
|
||||
unset_empty(&self.smtp.user),
|
||||
if !self.smtp.password.is_empty() {
|
||||
pw
|
||||
} else {
|
||||
unset
|
||||
},
|
||||
unset_empty(&self.smtp.server),
|
||||
self.smtp.port,
|
||||
self.smtp.certificate_checks,
|
||||
flags_readable,
|
||||
)
|
||||
}
|
||||
@@ -237,30 +277,6 @@ fn get_readable_flags(flags: i32) -> String {
|
||||
res += "AUTH_NORMAL ";
|
||||
flag_added = true;
|
||||
}
|
||||
if 1 << bit == 0x100 {
|
||||
res += "IMAP_STARTTLS ";
|
||||
flag_added = true;
|
||||
}
|
||||
if 1 << bit == 0x200 {
|
||||
res += "IMAP_SSL ";
|
||||
flag_added = true;
|
||||
}
|
||||
if 1 << bit == 0x400 {
|
||||
res += "IMAP_PLAIN ";
|
||||
flag_added = true;
|
||||
}
|
||||
if 1 << bit == 0x10000 {
|
||||
res += "SMTP_STARTTLS ";
|
||||
flag_added = true;
|
||||
}
|
||||
if 1 << bit == 0x20000 {
|
||||
res += "SMTP_SSL ";
|
||||
flag_added = true;
|
||||
}
|
||||
if 1 << bit == 0x40000 {
|
||||
res += "SMTP_PLAIN ";
|
||||
flag_added = true;
|
||||
}
|
||||
if flag_added {
|
||||
res += &format!("{:#0x}", 1 << bit);
|
||||
}
|
||||
|
||||
182
src/message.rs
@@ -12,7 +12,7 @@ use crate::contact::*;
|
||||
use crate::context::*;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::{ensure, Error};
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::job::{self, Action};
|
||||
use crate::lot::{Lot, LotState, Meaning};
|
||||
use crate::mimeparser::{FailureReport, SystemMessage};
|
||||
@@ -643,8 +643,45 @@ impl Message {
|
||||
None
|
||||
}
|
||||
|
||||
// add room to a webrtc_instance as defined by the corresponding config-value;
|
||||
// the result may still be prefixed by the type
|
||||
pub fn create_webrtc_instance(instance: &str, room: &str) -> String {
|
||||
let (videochat_type, mut url) = Message::parse_webrtc_instance(instance);
|
||||
|
||||
// make sure, there is a scheme in the url
|
||||
if !url.contains(':') {
|
||||
url = format!("https://{}", url);
|
||||
}
|
||||
|
||||
// add/replace room
|
||||
let url = if url.contains("$ROOM") {
|
||||
url.replace("$ROOM", &room)
|
||||
} else {
|
||||
// if there nothing that would separate the room, add a slash as a separator;
|
||||
// this way, urls can be given as "https://meet.jit.si" as well as "https://meet.jit.si/"
|
||||
let maybe_slash = if url.ends_with('/')
|
||||
|| url.ends_with('?')
|
||||
|| url.ends_with('#')
|
||||
|| url.ends_with('=')
|
||||
{
|
||||
""
|
||||
} else {
|
||||
"/"
|
||||
};
|
||||
format!("{}{}{}", url, maybe_slash, room)
|
||||
};
|
||||
|
||||
// re-add and normalize type
|
||||
match videochat_type {
|
||||
VideochatType::BasicWebrtc => format!("basicwebrtc:{}", url),
|
||||
VideochatType::Jitsi => format!("jitsi:{}", url),
|
||||
VideochatType::Unknown => url,
|
||||
}
|
||||
}
|
||||
|
||||
/// split a webrtc_instance as defined by the corresponding config-value into a type and a url
|
||||
pub fn parse_webrtc_instance(instance: &str) -> (VideochatType, String) {
|
||||
let instance: String = instance.split_whitespace().collect();
|
||||
let mut split = instance.splitn(2, ':');
|
||||
let type_str = split.next().unwrap_or_default().to_lowercase();
|
||||
let url = split.next();
|
||||
@@ -1059,18 +1096,70 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
|
||||
pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
||||
let extension: &str = &path.extension()?.to_str()?.to_lowercase();
|
||||
let info = match extension {
|
||||
"mp3" => (Viewtype::Audio, "audio/mpeg"),
|
||||
// before using viewtype other than Viewtype::File,
|
||||
// make sure, all target UIs support that type in the context of the used viewer/player.
|
||||
// if in doubt, it is better to default to Viewtype::File that passes handing to an external app.
|
||||
// (cmp. https://developer.android.com/guide/topics/media/media-formats )
|
||||
"3gp" => (Viewtype::Video, "video/3gpp"),
|
||||
"aac" => (Viewtype::Audio, "audio/aac"),
|
||||
"mp4" => (Viewtype::Video, "video/mp4"),
|
||||
"webm" => (Viewtype::Video, "video/webm"),
|
||||
"jpg" => (Viewtype::Image, "image/jpeg"),
|
||||
"avi" => (Viewtype::Video, "video/x-msvideo"),
|
||||
"doc" => (Viewtype::File, "application/msword"),
|
||||
"docx" => (
|
||||
Viewtype::File,
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
),
|
||||
"epub" => (Viewtype::File, "application/epub+zip"),
|
||||
"flac" => (Viewtype::Audio, "audio/flac"),
|
||||
"gif" => (Viewtype::Gif, "image/gif"),
|
||||
"html" => (Viewtype::File, "text/html"),
|
||||
"htm" => (Viewtype::File, "text/html"),
|
||||
"ico" => (Viewtype::File, "image/vnd.microsoft.icon"),
|
||||
"jar" => (Viewtype::File, "application/java-archive"),
|
||||
"jpeg" => (Viewtype::Image, "image/jpeg"),
|
||||
"jpe" => (Viewtype::Image, "image/jpeg"),
|
||||
"jpg" => (Viewtype::Image, "image/jpeg"),
|
||||
"json" => (Viewtype::File, "application/json"),
|
||||
"mov" => (Viewtype::Video, "video/quicktime"),
|
||||
"mp3" => (Viewtype::Audio, "audio/mpeg"),
|
||||
"mp4" => (Viewtype::Video, "video/mp4"),
|
||||
"odp" => (
|
||||
Viewtype::File,
|
||||
"application/vnd.oasis.opendocument.presentation",
|
||||
),
|
||||
"ods" => (
|
||||
Viewtype::File,
|
||||
"application/vnd.oasis.opendocument.spreadsheet",
|
||||
),
|
||||
"odt" => (Viewtype::File, "application/vnd.oasis.opendocument.text"),
|
||||
"oga" => (Viewtype::Audio, "audio/ogg"),
|
||||
"ogg" => (Viewtype::Audio, "audio/ogg"),
|
||||
"ogv" => (Viewtype::File, "video/ogg"),
|
||||
"opus" => (Viewtype::File, "audio/ogg"), // not supported eg. on Android 4
|
||||
"otf" => (Viewtype::File, "font/otf"),
|
||||
"pdf" => (Viewtype::File, "application/pdf"),
|
||||
"png" => (Viewtype::Image, "image/png"),
|
||||
"webp" => (Viewtype::Image, "image/webp"),
|
||||
"gif" => (Viewtype::Gif, "image/gif"),
|
||||
"vcf" => (Viewtype::File, "text/vcard"),
|
||||
"rar" => (Viewtype::File, "application/vnd.rar"),
|
||||
"rtf" => (Viewtype::File, "application/rtf"),
|
||||
"spx" => (Viewtype::File, "audio/ogg"), // Ogg Speex Profile
|
||||
"svg" => (Viewtype::File, "image/svg+xml"),
|
||||
"tgs" => (Viewtype::Sticker, "application/x-tgsticker"),
|
||||
"tiff" => (Viewtype::File, "image/tiff"),
|
||||
"tif" => (Viewtype::File, "image/tiff"),
|
||||
"ttf" => (Viewtype::File, "font/ttf"),
|
||||
"vcard" => (Viewtype::File, "text/vcard"),
|
||||
"vcf" => (Viewtype::File, "text/vcard"),
|
||||
"wav" => (Viewtype::File, "audio/wav"),
|
||||
"weba" => (Viewtype::File, "audio/webm"),
|
||||
"webm" => (Viewtype::Video, "video/webm"),
|
||||
"webp" => (Viewtype::Image, "image/webp"), // iOS via SDWebImage, Android since 4.0
|
||||
"wmv" => (Viewtype::Video, "video/x-ms-wmv"),
|
||||
"xhtml" => (Viewtype::File, "application/xhtml+xml"),
|
||||
"xlsx" => (
|
||||
Viewtype::File,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
),
|
||||
"xml" => (Viewtype::File, "application/vnd.ms-excel"),
|
||||
"zip" => (Viewtype::File, "application/zip"),
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
@@ -1107,7 +1196,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
|
||||
}
|
||||
|
||||
if !msg_ids.is_empty() {
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
@@ -1198,7 +1287,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> bool {
|
||||
}
|
||||
|
||||
if send_event {
|
||||
context.emit_event(Event::MsgsChanged {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
@@ -1365,7 +1454,7 @@ pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => context.emit_event(Event::MsgFailed {
|
||||
Ok(_) => context.emit_event(EventType::MsgFailed {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id,
|
||||
}),
|
||||
@@ -1540,7 +1629,7 @@ pub(crate) async fn handle_ndn(
|
||||
.await,
|
||||
)
|
||||
.await;
|
||||
context.emit_event(Event::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1721,6 +1810,7 @@ pub async fn dc_empty_server(context: &Context, flags: u32) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::ChatItem;
|
||||
use crate::test_utils as test;
|
||||
|
||||
#[test]
|
||||
@@ -1863,4 +1953,72 @@ mod tests {
|
||||
assert_eq!(webrtc_type, VideochatType::Jitsi);
|
||||
assert_eq!(url, "https://j.si/foo");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_create_webrtc_instance() {
|
||||
// webrtc_instance may come from an input field of the ui, be pretty tolerant on input
|
||||
let instance = Message::create_webrtc_instance("https://meet.jit.si/", "123");
|
||||
assert_eq!(instance, "https://meet.jit.si/123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("https://meet.jit.si", "456");
|
||||
assert_eq!(instance, "https://meet.jit.si/456");
|
||||
|
||||
let instance = Message::create_webrtc_instance("meet.jit.si", "789");
|
||||
assert_eq!(instance, "https://meet.jit.si/789");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo?", "123");
|
||||
assert_eq!(instance, "https://bla.foo?123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("jitsi:bla.foo#", "456");
|
||||
assert_eq!(instance, "jitsi:https://bla.foo#456");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room=", "789");
|
||||
assert_eq!(instance, "https://bla.foo#room=789");
|
||||
|
||||
let instance = Message::create_webrtc_instance("https://bla.foo#room", "123");
|
||||
assert_eq!(instance, "https://bla.foo#room/123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room$ROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo#room123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room=$ROOM&after=cont", "234");
|
||||
assert_eq!(instance, "https://bla.foo#room=234&after=cont");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" meet.jit .si ", "789");
|
||||
assert_eq!(instance, "https://meet.jit.si/789");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" basicwebrtc: basic . stuff\n ", "12345ab");
|
||||
assert_eq!(instance, "basicwebrtc:https://basic.stuff/12345ab");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_width_height() {
|
||||
let t = test::TestContext::new().await;
|
||||
|
||||
// test that get_width() and get_height() are returning some dimensions for images;
|
||||
// (as the device-chat contains a welcome-images, we check that)
|
||||
t.ctx.update_device_chats().await.ok();
|
||||
let (device_chat_id, _) =
|
||||
chat::create_or_lookup_by_contact_id(&t.ctx, DC_CONTACT_ID_DEVICE, Blocked::Not)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut has_image = false;
|
||||
let chatitems = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
|
||||
for chatitem in chatitems {
|
||||
if let ChatItem::Message { msg_id } = chatitem {
|
||||
if let Ok(msg) = Message::load_from_db(&t.ctx, msg_id.clone()).await {
|
||||
if msg.get_viewtype() == Viewtype::Image {
|
||||
has_image = true;
|
||||
// just check that width/height are inside some reasonable ranges
|
||||
assert!(msg.get_width() > 100);
|
||||
assert!(msg.get_height() > 100);
|
||||
assert!(msg.get_width() < 4000);
|
||||
assert!(msg.get_height() < 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(has_image);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::dc_tools::*;
|
||||
use crate::e2ee::*;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::error::{bail, ensure, format_err, Error};
|
||||
use crate::format_flowed::format_flowed;
|
||||
use crate::location;
|
||||
use crate::message::{self, Message};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -221,7 +222,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
.filter(|(_, addr)| addr != &self_addr)
|
||||
{
|
||||
res.push((
|
||||
Peerstate::from_addr(self.context, addr).await,
|
||||
Peerstate::from_addr(self.context, addr).await?,
|
||||
addr.as_str(),
|
||||
));
|
||||
}
|
||||
@@ -453,21 +454,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
|
||||
unprotected_headers.push(Header::new("Date".into(), date));
|
||||
|
||||
let os_name = &self.context.os_name;
|
||||
let os_part = os_name
|
||||
.as_ref()
|
||||
.map(|s| format!("/{}", s))
|
||||
.unwrap_or_default();
|
||||
let version = get_version_str();
|
||||
|
||||
// Add a X-Mailer header.
|
||||
// This is only informational for debugging and may be removed in the release.
|
||||
// We do not rely on this header as it may be removed by MTAs.
|
||||
|
||||
unprotected_headers.push(Header::new(
|
||||
"X-Mailer".into(),
|
||||
format!("Delta Chat Core {}{}", version, os_part),
|
||||
));
|
||||
unprotected_headers.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
|
||||
|
||||
if let Loaded::MDN { .. } = self.loaded {
|
||||
@@ -925,11 +911,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
};
|
||||
|
||||
let flowed_text = format_flowed(final_text);
|
||||
|
||||
let footer = &self.selfstatus;
|
||||
let message_text = format!(
|
||||
"{}{}{}{}{}",
|
||||
fwdhint.unwrap_or_default(),
|
||||
escape_message_footer_marks(final_text),
|
||||
escape_message_footer_marks(&flowed_text),
|
||||
if !final_text.is_empty() && !footer.is_empty() {
|
||||
"\r\n\r\n"
|
||||
} else {
|
||||
@@ -941,7 +929,10 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
|
||||
// Message is sent as text/plain, with charset = utf-8
|
||||
let main_part = PartBuilder::new()
|
||||
.content_type(&mime::TEXT_PLAIN_UTF_8)
|
||||
.header((
|
||||
"Content-Type".to_string(),
|
||||
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
|
||||
))
|
||||
.body(message_text);
|
||||
let mut parts = Vec::new();
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ use crate::dc_tools::*;
|
||||
use crate::dehtml::dehtml;
|
||||
use crate::e2ee;
|
||||
use crate::error::{bail, Result};
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::format_flowed::unformat_flowed;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::Fingerprint;
|
||||
use crate::location;
|
||||
use crate::message;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::securejoin::handle_degrade_event;
|
||||
use crate::simplify::*;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
@@ -128,65 +128,74 @@ impl MimeMessage {
|
||||
let mail_raw;
|
||||
let mut gossipped_addr = Default::default();
|
||||
|
||||
let (mail, signatures) = match e2ee::try_decrypt(context, &mail, message_time).await {
|
||||
Ok((raw, signatures)) => {
|
||||
if let Some(raw) = raw {
|
||||
// Encrypted, but maybe unsigned message. Only if
|
||||
// `signatures` set is non-empty, it is a valid
|
||||
// autocrypt message.
|
||||
let (mail, signatures, warn_empty_signature) =
|
||||
match e2ee::try_decrypt(context, &mail, message_time).await {
|
||||
Ok((raw, signatures)) => {
|
||||
if let Some(raw) = raw {
|
||||
// Encrypted, but maybe unsigned message. Only if
|
||||
// `signatures` set is non-empty, it is a valid
|
||||
// autocrypt message.
|
||||
|
||||
mail_raw = raw;
|
||||
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(context, "decrypted message mime-body:");
|
||||
println!("{}", String::from_utf8_lossy(&mail_raw));
|
||||
mail_raw = raw;
|
||||
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(context, "decrypted message mime-body:");
|
||||
println!("{}", String::from_utf8_lossy(&mail_raw));
|
||||
}
|
||||
|
||||
// Handle any gossip headers if the mail was encrypted. See section
|
||||
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
|
||||
// but only if the mail was correctly signed:
|
||||
if !signatures.is_empty() {
|
||||
let gossip_headers =
|
||||
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossipped_addr = update_gossip_peerstates(
|
||||
context,
|
||||
message_time,
|
||||
&mail,
|
||||
gossip_headers,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// let known protected headers from the decrypted
|
||||
// part override the unencrypted top-level
|
||||
|
||||
// Signature was checked for original From, so we
|
||||
// do not allow overriding it.
|
||||
let mut throwaway_from = from.clone();
|
||||
|
||||
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
|
||||
// See https://github.com/deltachat/deltachat-core-rust/issues/1790.
|
||||
headers.remove("subject");
|
||||
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut throwaway_from,
|
||||
&mut chat_disposition_notification_to,
|
||||
&decrypted_mail.headers,
|
||||
);
|
||||
|
||||
(decrypted_mail, signatures, true)
|
||||
} else {
|
||||
// Message was not encrypted
|
||||
(mail, signatures, false)
|
||||
}
|
||||
|
||||
// Handle any gossip headers if the mail was encrypted. See section
|
||||
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
|
||||
// but only if the mail was correctly signed:
|
||||
if !signatures.is_empty() {
|
||||
let gossip_headers =
|
||||
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossipped_addr =
|
||||
update_gossip_peerstates(context, message_time, &mail, gossip_headers)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// let known protected headers from the decrypted
|
||||
// part override the unencrypted top-level
|
||||
|
||||
// Signature was checked for original From, so we
|
||||
// do not allow overriding it.
|
||||
let mut throwaway_from = from.clone();
|
||||
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut recipients,
|
||||
&mut throwaway_from,
|
||||
&mut chat_disposition_notification_to,
|
||||
&decrypted_mail.headers,
|
||||
);
|
||||
|
||||
(decrypted_mail, signatures)
|
||||
} else {
|
||||
// Message was not encrypted
|
||||
(mail, signatures)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// continue with the current, still encrypted, mime tree.
|
||||
// unencrypted parts will be replaced by an error message
|
||||
// that is added as "the message" to the chat then.
|
||||
//
|
||||
// if we just return here, the header is missing
|
||||
// and the caller cannot display the message
|
||||
// and try to assign the message to a chat
|
||||
warn!(context, "decryption failed: {}", err);
|
||||
(mail, Default::default())
|
||||
}
|
||||
};
|
||||
Err(err) => {
|
||||
// continue with the current, still encrypted, mime tree.
|
||||
// unencrypted parts will be replaced by an error message
|
||||
// that is added as "the message" to the chat then.
|
||||
//
|
||||
// if we just return here, the header is missing
|
||||
// and the caller cannot display the message
|
||||
// and try to assign the message to a chat
|
||||
warn!(context, "decryption failed: {}", err);
|
||||
(mail, Default::default(), true)
|
||||
}
|
||||
};
|
||||
|
||||
let mut parser = MimeMessage {
|
||||
parts: Vec::new(),
|
||||
@@ -212,7 +221,7 @@ impl MimeMessage {
|
||||
parser.heuristically_parse_ndn(context).await;
|
||||
parser.parse_headers(context)?;
|
||||
|
||||
if parser.signatures.is_empty() {
|
||||
if warn_empty_signature && parser.signatures.is_empty() {
|
||||
for part in parser.parts.iter_mut() {
|
||||
part.error = "No valid signature".to_string();
|
||||
}
|
||||
@@ -596,7 +605,7 @@ impl MimeMessage {
|
||||
skip the rest. (see
|
||||
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
|
||||
for background information why we use encrypted+signed) */
|
||||
if let Some(first) = mail.subparts.iter().next() {
|
||||
if let Some(first) = mail.subparts.get(0) {
|
||||
any_part_added = self.parse_mime_recursive(context, first).await?;
|
||||
}
|
||||
}
|
||||
@@ -633,7 +642,7 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
if let Some(first) = mail.subparts.iter().next() {
|
||||
if let Some(first) = mail.subparts.get(0) {
|
||||
any_part_added = self.parse_mime_recursive(context, first).await?;
|
||||
}
|
||||
}
|
||||
@@ -707,6 +716,27 @@ impl MimeMessage {
|
||||
simplify(out, self.has_chat_version())
|
||||
};
|
||||
|
||||
let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
|
||||
{
|
||||
format.as_str().to_ascii_lowercase() == "flowed"
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let simplified_txt = if mime_type.type_() == mime::TEXT
|
||||
&& mime_type.subtype() == mime::PLAIN
|
||||
&& is_format_flowed
|
||||
{
|
||||
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
|
||||
delsp.as_str().to_ascii_lowercase() == "yes"
|
||||
} else {
|
||||
false
|
||||
};
|
||||
unformat_flowed(&simplified_txt, delsp)
|
||||
} else {
|
||||
simplified_txt
|
||||
};
|
||||
|
||||
if !simplified_txt.is_empty() {
|
||||
let mut part = Part::default();
|
||||
part.typ = Viewtype::Text;
|
||||
@@ -1001,22 +1031,41 @@ impl MimeMessage {
|
||||
if let Some((chat_id, msg_id)) =
|
||||
message::handle_mdn(context, from_id, original_message_id, sent_timestamp).await
|
||||
{
|
||||
context.emit_event(Event::MsgRead { chat_id, msg_id });
|
||||
context.emit_event(EventType::MsgRead { chat_id, msg_id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(failure_report) = &self.failure_report {
|
||||
let error = parts.iter().find(|p| p.typ == Viewtype::Text).map(|p| {
|
||||
let msg = &p.msg;
|
||||
msg.find("\n--- ")
|
||||
.and_then(|footer_start| msg.get(..footer_start))
|
||||
.unwrap_or(msg)
|
||||
.trim()
|
||||
});
|
||||
let error = parts
|
||||
.iter()
|
||||
.find(|p| p.typ == Viewtype::Text)
|
||||
.map(|p| p.msg.clone());
|
||||
message::handle_ndn(context, failure_report, error).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns timestamp of the parent message.
|
||||
///
|
||||
/// If there is no parent message or it is not found in the
|
||||
/// database, returns None.
|
||||
pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
|
||||
let parent_timestamp = if let Some(field) = self
|
||||
.get(HeaderDef::InReplyTo)
|
||||
.and_then(|msgid| parse_message_id(msgid).ok())
|
||||
{
|
||||
context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT timestamp FROM msgs WHERE rfc724_mid=?",
|
||||
paramsv![field],
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(parent_timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_gossip_peerstates(
|
||||
@@ -1036,7 +1085,7 @@ async fn update_gossip_peerstates(
|
||||
.iter()
|
||||
.any(|info| info.addr == header.addr.to_lowercase())
|
||||
{
|
||||
let mut peerstate = Peerstate::from_addr(context, &header.addr).await;
|
||||
let mut peerstate = Peerstate::from_addr(context, &header.addr).await?;
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
peerstate.apply_gossip(header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
@@ -1046,9 +1095,7 @@ async fn update_gossip_peerstates(
|
||||
peerstate = Some(p);
|
||||
}
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.degrade_event.is_some() {
|
||||
handle_degrade_event(context, &peerstate).await?;
|
||||
}
|
||||
peerstate.handle_fingerprint_change(context).await?;
|
||||
}
|
||||
|
||||
gossipped_addr.insert(header.addr.clone());
|
||||
@@ -1388,6 +1435,39 @@ mod tests {
|
||||
assert!(mimeparser.chat_disposition_notification_to.is_none());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_parent_timestamp() {
|
||||
let context = TestContext::new().await;
|
||||
let raw = b"From: foo@example.org\n\
|
||||
Content-Type: text/plain\n\
|
||||
Chat-Version: 1.0\n\
|
||||
In-Reply-To: <Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org>\n\
|
||||
\n\
|
||||
Some reply\n\
|
||||
";
|
||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
mimeparser.get_parent_timestamp(&context.ctx).await.unwrap(),
|
||||
None
|
||||
);
|
||||
let timestamp = 1570435529;
|
||||
context
|
||||
.ctx
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)",
|
||||
paramsv!["Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp],
|
||||
)
|
||||
.await
|
||||
.expect("Failed to write to the database");
|
||||
assert_eq!(
|
||||
mimeparser.get_parent_timestamp(&context.ctx).await.unwrap(),
|
||||
Some(timestamp)
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_mimeparser_with_context() {
|
||||
let context = TestContext::new().await;
|
||||
@@ -2026,4 +2106,13 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||
let test = parse_message_ids(" < ").unwrap();
|
||||
assert!(test.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mime_parse_format_flowed() {
|
||||
let mime_type = "text/plain; charset=utf-8; Format=Flowed; DelSp=No"
|
||||
.parse::<mime::Mime>()
|
||||
.unwrap();
|
||||
let format_param = mime_type.get_param("format").unwrap();
|
||||
assert_eq!(format_param.as_str().to_ascii_lowercase(), "flowed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,12 @@ pub enum Param {
|
||||
Recipients = b'R',
|
||||
|
||||
/// For Groups
|
||||
///
|
||||
/// An unpromoted group has not had any messages sent to it and thus only exists on the
|
||||
/// creator's device. Any changes made to an unpromoted group do not need to send
|
||||
/// system messages to the group members to update them of the changes. Once a message
|
||||
/// has been sent to a group it is promoted and group changes require sending system
|
||||
/// messages to all members.
|
||||
Unpromoted = b'U',
|
||||
|
||||
/// For Groups and Contacts
|
||||
|
||||
159
src/peerstate.rs
@@ -5,9 +5,14 @@ use std::fmt;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::*;
|
||||
use crate::chat;
|
||||
use crate::constants::Blocked;
|
||||
use crate::context::Context;
|
||||
use crate::error::{bail, Result};
|
||||
use crate::events::EventType;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::sql::Sql;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PeerstateKeyType {
|
||||
@@ -38,7 +43,7 @@ pub struct Peerstate<'a> {
|
||||
pub verified_key: Option<SignedPublicKey>,
|
||||
pub verified_key_fingerprint: Option<Fingerprint>,
|
||||
pub to_save: Option<ToSave>,
|
||||
pub degrade_event: Option<DegradeEvent>,
|
||||
pub fingerprint_changed: bool,
|
||||
}
|
||||
|
||||
impl<'a> PartialEq for Peerstate<'a> {
|
||||
@@ -55,7 +60,7 @@ impl<'a> PartialEq for Peerstate<'a> {
|
||||
&& self.verified_key == other.verified_key
|
||||
&& self.verified_key_fingerprint == other.verified_key_fingerprint
|
||||
&& self.to_save == other.to_save
|
||||
&& self.degrade_event == other.degrade_event
|
||||
&& self.fingerprint_changed == other.fingerprint_changed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +81,7 @@ impl<'a> fmt::Debug for Peerstate<'a> {
|
||||
.field("verified_key", &self.verified_key)
|
||||
.field("verified_key_fingerprint", &self.verified_key_fingerprint)
|
||||
.field("to_save", &self.to_save)
|
||||
.field("degrade_event", &self.degrade_event)
|
||||
.field("fingerprint_changed", &self.fingerprint_changed)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -88,16 +93,6 @@ pub enum ToSave {
|
||||
All = 0x02,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum DegradeEvent {
|
||||
/// Recoverable by an incoming encrypted mail.
|
||||
EncryptionPaused = 0x01,
|
||||
|
||||
/// Recoverable by a new verify.
|
||||
FingerprintChanged = 0x02,
|
||||
}
|
||||
|
||||
impl<'a> Peerstate<'a> {
|
||||
pub fn new(context: &'a Context, addr: String) -> Self {
|
||||
Peerstate {
|
||||
@@ -114,7 +109,7 @@ impl<'a> Peerstate<'a> {
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: None,
|
||||
degrade_event: None,
|
||||
fingerprint_changed: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,8 +137,12 @@ impl<'a> Peerstate<'a> {
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn from_addr(context: &'a Context, addr: &str) -> Option<Peerstate<'a>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, verified_key, verified_key_fingerprint FROM acpeerstates WHERE addr=? COLLATE NOCASE;";
|
||||
pub async fn from_addr(context: &'a Context, addr: &str) -> Result<Option<Peerstate<'a>>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||
verified_key, verified_key_fingerprint \
|
||||
FROM acpeerstates \
|
||||
WHERE addr=? COLLATE NOCASE;";
|
||||
Self::from_stmt(context, query, paramsv![addr]).await
|
||||
}
|
||||
|
||||
@@ -151,7 +150,7 @@ impl<'a> Peerstate<'a> {
|
||||
context: &'a Context,
|
||||
_sql: &Sql,
|
||||
fingerprint: &Fingerprint,
|
||||
) -> Option<Peerstate<'a>> {
|
||||
) -> Result<Option<Peerstate<'a>>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||
verified_key, verified_key_fingerprint \
|
||||
@@ -167,10 +166,10 @@ impl<'a> Peerstate<'a> {
|
||||
context: &'a Context,
|
||||
query: &str,
|
||||
params: Vec<&dyn crate::ToSql>,
|
||||
) -> Option<Peerstate<'a>> {
|
||||
context
|
||||
) -> Result<Option<Peerstate<'a>>> {
|
||||
let peerstate = context
|
||||
.sql
|
||||
.query_row(query, params, |row| {
|
||||
.query_row_optional(query, params, |row| {
|
||||
/* all the above queries start with this: SELECT
|
||||
addr, last_seen, last_seen_autocrypt, prefer_encrypted,
|
||||
public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
|
||||
@@ -186,15 +185,18 @@ impl<'a> Peerstate<'a> {
|
||||
res.public_key_fingerprint = row
|
||||
.get::<_, Option<String>>(7)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()?;
|
||||
.transpose()
|
||||
.unwrap_or_default();
|
||||
res.gossip_key_fingerprint = row
|
||||
.get::<_, Option<String>>(8)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()?;
|
||||
.transpose()
|
||||
.unwrap_or_default();
|
||||
res.verified_key_fingerprint = row
|
||||
.get::<_, Option<String>>(10)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()?;
|
||||
.transpose()
|
||||
.unwrap_or_default();
|
||||
res.public_key = row
|
||||
.get(4)
|
||||
.ok()
|
||||
@@ -210,8 +212,8 @@ impl<'a> Peerstate<'a> {
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
.await?;
|
||||
Ok(peerstate)
|
||||
}
|
||||
|
||||
pub fn recalc_fingerprint(&mut self) {
|
||||
@@ -225,7 +227,7 @@ impl<'a> Peerstate<'a> {
|
||||
{
|
||||
self.to_save = Some(ToSave::All);
|
||||
if old_public_fingerprint.is_some() {
|
||||
self.degrade_event = Some(DegradeEvent::FingerprintChanged);
|
||||
self.fingerprint_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,23 +241,51 @@ impl<'a> Peerstate<'a> {
|
||||
|| old_gossip_fingerprint != self.gossip_key_fingerprint
|
||||
{
|
||||
self.to_save = Some(ToSave::All);
|
||||
if old_gossip_fingerprint.is_some() {
|
||||
self.degrade_event = Some(DegradeEvent::FingerprintChanged);
|
||||
|
||||
// Warn about gossip key change only if there is no public key obtained from
|
||||
// Autocrypt header, which overrides gossip key.
|
||||
if old_gossip_fingerprint.is_some() && self.public_key_fingerprint.is_none() {
|
||||
self.fingerprint_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn degrade_encryption(&mut self, message_time: i64) {
|
||||
if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
self.degrade_event = Some(DegradeEvent::EncryptionPaused);
|
||||
}
|
||||
|
||||
self.prefer_encrypt = EncryptPreference::Reset;
|
||||
self.last_seen = message_time;
|
||||
self.to_save = Some(ToSave::All);
|
||||
}
|
||||
|
||||
/// Adds a warning to the chat corresponding to peerstate if fingerprint has changed.
|
||||
pub(crate) async fn handle_fingerprint_change(&self, context: &Context) -> Result<()> {
|
||||
if self.fingerprint_changed {
|
||||
if let Some(contact_id) = context
|
||||
.sql
|
||||
.query_get_value_result(
|
||||
"SELECT id FROM contacts WHERE addr=?;",
|
||||
paramsv![self.addr],
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let (contact_chat_id, _) =
|
||||
chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Deaddrop)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let msg = context
|
||||
.stock_string_repl_str(StockMessage::ContactSetupChanged, self.addr.clone())
|
||||
.await;
|
||||
|
||||
chat::add_info_msg(context, contact_chat_id, msg).await;
|
||||
emit_event!(context, EventType::ChatModified(contact_chat_id));
|
||||
} else {
|
||||
bail!("contact with peerstate.addr {:?} not found", &self.addr);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_header(&mut self, header: &Aheader, message_time: i64) {
|
||||
if self.addr.to_lowercase() != header.addr.to_lowercase() {
|
||||
return;
|
||||
@@ -269,11 +299,6 @@ impl<'a> Peerstate<'a> {
|
||||
|| header.prefer_encrypt == EncryptPreference::NoPreference)
|
||||
&& header.prefer_encrypt != self.prefer_encrypt
|
||||
{
|
||||
if self.prefer_encrypt == EncryptPreference::Mutual
|
||||
&& header.prefer_encrypt != EncryptPreference::Mutual
|
||||
{
|
||||
self.degrade_event = Some(DegradeEvent::EncryptionPaused);
|
||||
}
|
||||
self.prefer_encrypt = header.prefer_encrypt;
|
||||
self.to_save = Some(ToSave::All)
|
||||
}
|
||||
@@ -400,21 +425,20 @@ impl<'a> Peerstate<'a> {
|
||||
}
|
||||
|
||||
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> {
|
||||
if create {
|
||||
sql.execute(
|
||||
"INSERT INTO acpeerstates (addr) VALUES(?);",
|
||||
paramsv![self.addr],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if self.to_save == Some(ToSave::All) || create {
|
||||
sql.execute(
|
||||
if create {
|
||||
"INSERT INTO acpeerstates (last_seen, last_seen_autocrypt, prefer_encrypted, \
|
||||
public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||
verified_key, verified_key_fingerprint, addr \
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
|
||||
} else {
|
||||
"UPDATE acpeerstates \
|
||||
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \
|
||||
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \
|
||||
verified_key=?, verified_key_fingerprint=? \
|
||||
WHERE addr=?;",
|
||||
WHERE addr=?"
|
||||
},
|
||||
paramsv![
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
@@ -489,7 +513,7 @@ mod tests {
|
||||
verified_key: Some(pub_key.clone()),
|
||||
verified_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
to_save: Some(ToSave::All),
|
||||
degrade_event: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
|
||||
assert!(
|
||||
@@ -499,7 +523,8 @@ mod tests {
|
||||
|
||||
let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr)
|
||||
.await
|
||||
.expect("failed to load peerstate from db");
|
||||
.expect("failed to load peerstate from db")
|
||||
.expect("no peerstate found in the database");
|
||||
|
||||
// clear to_save, as that is not persissted
|
||||
peerstate.to_save = None;
|
||||
@@ -507,7 +532,8 @@ mod tests {
|
||||
let peerstate_new2 =
|
||||
Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint())
|
||||
.await
|
||||
.expect("failed to load peerstate from db");
|
||||
.expect("failed to load peerstate from db")
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate, peerstate_new2);
|
||||
}
|
||||
|
||||
@@ -531,7 +557,7 @@ mod tests {
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
degrade_event: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
|
||||
assert!(
|
||||
@@ -565,7 +591,7 @@ mod tests {
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
degrade_event: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
|
||||
assert!(
|
||||
@@ -579,7 +605,36 @@ mod tests {
|
||||
|
||||
// clear to_save, as that is not persissted
|
||||
peerstate.to_save = None;
|
||||
assert_eq!(peerstate, peerstate_new);
|
||||
assert_eq!(Some(peerstate), peerstate_new);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_peerstate_load_db_defaults() {
|
||||
let ctx = crate::test_utils::TestContext::new().await;
|
||||
let addr = "hello@mail.com";
|
||||
|
||||
// Old code created peerstates with this code and updated
|
||||
// other values later. If UPDATE failed, other columns had
|
||||
// default values, in particular fingerprints were set to
|
||||
// empty strings instead of NULL. This should not be the case
|
||||
// anymore, but the regression test still checks that defaults
|
||||
// can be loaded without errors.
|
||||
ctx.ctx
|
||||
.sql
|
||||
.execute("INSERT INTO acpeerstates (addr) VALUES(?)", paramsv![addr])
|
||||
.await
|
||||
.expect("Failed to write to the database");
|
||||
|
||||
let peerstate = Peerstate::from_addr(&ctx.ctx, addr)
|
||||
.await
|
||||
.expect("Failed to load peerstate from db")
|
||||
.expect("Loaded peerstate is empty");
|
||||
|
||||
// Check that default values for fingerprints are treated like
|
||||
// NULL.
|
||||
assert_eq!(peerstate.public_key_fingerprint, None);
|
||||
assert_eq!(peerstate.gossip_key_fingerprint, None);
|
||||
assert_eq!(peerstate.verified_key_fingerprint, None);
|
||||
}
|
||||
|
||||
// TODO: don't copy this from stress.rs
|
||||
|
||||
@@ -81,6 +81,21 @@ lazy_static::lazy_static! {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// buzon.uy.md: buzon.uy
|
||||
static ref P_BUZON_UY: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/buzon-uy",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "buzon.uy", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "buzon.uy", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// chello.at.md: chello.at
|
||||
static ref P_CHELLO_AT: Provider = Provider {
|
||||
status: Status::OK,
|
||||
@@ -142,6 +157,9 @@ lazy_static::lazy_static! {
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/dubby-org",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "dubby.org", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "dubby.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" },
|
||||
@@ -181,6 +199,19 @@ lazy_static::lazy_static! {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// firemail.de.md: firemail.at, firemail.de
|
||||
static ref P_FIREMAIL_DE: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
before_login_hint: "Firemail erlaubt nur bei bezahlten Accounts den vollen Zugriff auf das E-Mail-Protokoll. Wenn Sie nicht für Firemail bezahlen, verwenden Sie bitte einen anderen E-Mail-Anbieter.",
|
||||
after_login_hint: "Leider schränkt Firemail die maximale Gruppengröße ein. Je nach Bezahlmodell sind nur 5 bis 30 Gruppenmitglieder erlaubt.",
|
||||
overview_page: "https://providers.delta.chat/firemail-de",
|
||||
server: vec![
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: false,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// five.chat.md: five.chat
|
||||
static ref P_FIVE_CHAT: Provider = Provider {
|
||||
status: Status::OK,
|
||||
@@ -188,6 +219,9 @@ lazy_static::lazy_static! {
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/five-chat",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "five.chat", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "five.chat", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "five.chat", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: Some(vec![
|
||||
ConfigDefault { key: Config::BccSelf, value: "1" },
|
||||
@@ -505,6 +539,21 @@ lazy_static::lazy_static! {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// undernet.uy.md: undernet.uy
|
||||
static ref P_UNDERNET_UY: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/undernet-uy",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "undernet.uy", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "undernet.uy", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// vfemail.md: vfemail.net
|
||||
static ref P_VFEMAIL: Provider = Provider {
|
||||
status: Status::OK,
|
||||
@@ -571,6 +620,8 @@ lazy_static::lazy_static! {
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/yandex-ru",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.yandex.com", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "smtp.yandex.com", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
config_defaults: None,
|
||||
strict_tls: true,
|
||||
@@ -598,6 +649,7 @@ lazy_static::lazy_static! {
|
||||
("arcor.de", &*P_ARCOR_DE),
|
||||
("autistici.org", &*P_AUTISTICI_ORG),
|
||||
("bluewin.ch", &*P_BLUEWIN_CH),
|
||||
("buzon.uy", &*P_BUZON_UY),
|
||||
("chello.at", &*P_CHELLO_AT),
|
||||
("xfinity.com", &*P_COMCAST),
|
||||
("comcast.net", &*P_COMCAST),
|
||||
@@ -607,6 +659,8 @@ lazy_static::lazy_static! {
|
||||
("example.com", &*P_EXAMPLE_COM),
|
||||
("example.org", &*P_EXAMPLE_COM),
|
||||
("fastmail.com", &*P_FASTMAIL),
|
||||
("firemail.at", &*P_FIREMAIL_DE),
|
||||
("firemail.de", &*P_FIREMAIL_DE),
|
||||
("five.chat", &*P_FIVE_CHAT),
|
||||
("freenet.de", &*P_FREENET_DE),
|
||||
("gmail.com", &*P_GMAIL),
|
||||
@@ -698,6 +752,7 @@ lazy_static::lazy_static! {
|
||||
("testrun.org", &*P_TESTRUN),
|
||||
("tiscali.it", &*P_TISCALI_IT),
|
||||
("ukr.net", &*P_UKR_NET),
|
||||
("undernet.uy", &*P_UNDERNET_UY),
|
||||
("vfemail.net", &*P_VFEMAIL),
|
||||
("vodafone.de", &*P_VODAFONE_DE),
|
||||
("vodafonemail.de", &*P_VODAFONE_DE),
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::config::Config;
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::provider::data::PROVIDER_DATA;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, ToPrimitive)]
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum Status {
|
||||
OK = 1,
|
||||
@@ -14,21 +14,29 @@ pub enum Status {
|
||||
BROKEN = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum Protocol {
|
||||
SMTP = 1,
|
||||
IMAP = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum Socket {
|
||||
STARTTLS = 1,
|
||||
SSL = 2,
|
||||
Automatic = 0,
|
||||
SSL = 1,
|
||||
STARTTLS = 2,
|
||||
Plain = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
impl Default for Socket {
|
||||
fn default() -> Self {
|
||||
Socket::Automatic
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum UsernamePattern {
|
||||
EMAIL = 1,
|
||||
@@ -42,7 +50,7 @@ pub enum Oauth2Authorizer {
|
||||
Gmail = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Server {
|
||||
pub protocol: Protocol,
|
||||
pub socket: Socket,
|
||||
@@ -51,20 +59,6 @@ pub struct Server {
|
||||
pub username_pattern: UsernamePattern,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn apply_username_pattern(&self, addr: String) -> String {
|
||||
match self.username_pattern {
|
||||
UsernamePattern::EMAIL => addr,
|
||||
UsernamePattern::EMAILLOCALPART => {
|
||||
if let Some(at) = addr.find('@') {
|
||||
return addr.split_at(at).0.to_string();
|
||||
}
|
||||
addr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConfigDefault {
|
||||
pub key: Config,
|
||||
@@ -83,25 +77,6 @@ pub struct Provider {
|
||||
pub oauth2_authorizer: Option<Oauth2Authorizer>,
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
pub fn get_server(&self, protocol: Protocol) -> Option<&Server> {
|
||||
for record in self.server.iter() {
|
||||
if record.protocol == protocol {
|
||||
return Some(record);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_imap_server(&self) -> Option<&Server> {
|
||||
self.get_server(Protocol::IMAP)
|
||||
}
|
||||
|
||||
pub fn get_smtp_server(&self) -> Option<&Server> {
|
||||
self.get_server(Protocol::SMTP)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_provider_info(addr: &str) -> Option<&Provider> {
|
||||
let domain = match addr.parse::<EmailAddress>() {
|
||||
Ok(addr) => addr.domain,
|
||||
@@ -137,15 +112,16 @@ mod tests {
|
||||
let provider = get_provider_info("nauta.cu"); // this is no email address
|
||||
assert!(provider.is_none());
|
||||
|
||||
let provider = get_provider_info("user@nauta.cu").unwrap();
|
||||
let addr = "user@nauta.cu";
|
||||
let provider = get_provider_info(addr).unwrap();
|
||||
assert!(provider.status == Status::OK);
|
||||
let server = provider.get_imap_server().unwrap();
|
||||
let server = &provider.server[0];
|
||||
assert_eq!(server.protocol, Protocol::IMAP);
|
||||
assert_eq!(server.socket, Socket::STARTTLS);
|
||||
assert_eq!(server.hostname, "imap.nauta.cu");
|
||||
assert_eq!(server.port, 143);
|
||||
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
|
||||
let server = provider.get_smtp_server().unwrap();
|
||||
let server = &provider.server[1];
|
||||
assert_eq!(server.protocol, Protocol::SMTP);
|
||||
assert_eq!(server.socket, Socket::STARTTLS);
|
||||
assert_eq!(server.hostname, "smtp.nauta.cu");
|
||||
|
||||
@@ -143,7 +143,10 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
let mut lot = Lot::new();
|
||||
|
||||
// retrieve known state for this fingerprint
|
||||
let peerstate = Peerstate::from_fingerprint(context, &context.sql, &fingerprint).await;
|
||||
let peerstate = match Peerstate::from_fingerprint(context, &context.sql, &fingerprint).await {
|
||||
Ok(peerstate) => peerstate,
|
||||
Err(err) => return format_err!("Can't load peerstate: {}", err).into(),
|
||||
};
|
||||
|
||||
if invitenumber.is_none() || auth.is_none() {
|
||||
if let Some(peerstate) = peerstate {
|
||||
|
||||
@@ -12,10 +12,10 @@ use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::*;
|
||||
use crate::error::{bail, Error};
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::lot::LotState;
|
||||
use crate::lot::{Lot, LotState};
|
||||
use crate::message::Message;
|
||||
use crate::mimeparser::*;
|
||||
use crate::param::*;
|
||||
@@ -32,7 +32,7 @@ macro_rules! joiner_progress {
|
||||
$progress >= 0 && $progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.emit_event($crate::events::Event::SecurejoinJoinerProgress {
|
||||
$context.emit_event($crate::events::EventType::SecurejoinJoinerProgress {
|
||||
contact_id: $contact_id,
|
||||
progress: $progress,
|
||||
});
|
||||
@@ -45,7 +45,7 @@ macro_rules! inviter_progress {
|
||||
$progress >= 0 && $progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.emit_event($crate::events::Event::SecurejoinInviterProgress {
|
||||
$context.emit_event($crate::events::EventType::SecurejoinInviterProgress {
|
||||
contact_id: $contact_id,
|
||||
progress: $progress,
|
||||
});
|
||||
@@ -67,6 +67,58 @@ macro_rules! get_qr_attr {
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum BobStatus {
|
||||
Error,
|
||||
Success,
|
||||
}
|
||||
|
||||
impl Default for BobStatus {
|
||||
fn default() -> Self {
|
||||
Self::Error
|
||||
}
|
||||
}
|
||||
|
||||
/// State for setup-contact/secure-join protocol joiner's side.
|
||||
///
|
||||
/// The setup-contact protocol needs to carry state for both the inviter (Alice) and the
|
||||
/// joiner/invitee (Bob). For Alice this state is minimal and in the `tokens` table in the
|
||||
/// database. For Bob this state is only carried live on the [Context] in this struct.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Bob {
|
||||
/// The next message expected by the protocol.
|
||||
expects: SecureJoinStep,
|
||||
/// The final status of the last-run setup-contact/secure-join protocol.
|
||||
///
|
||||
/// This is only meaningful if you know you have exited the protocol but have not
|
||||
/// started a new one.
|
||||
pub status: BobStatus,
|
||||
/// The QR-scanned information of the currently running protocol.
|
||||
pub qr_scan: Option<Lot>,
|
||||
}
|
||||
|
||||
/// The next message expected by [Bob] in the setup-contact/secure-join protocol.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum SecureJoinStep {
|
||||
/// No setup-contact protocol running.
|
||||
NotActive,
|
||||
/// Expecting the auth-required message.
|
||||
///
|
||||
/// This corresponds to the `vc-auth-required` or `vg-auth-required` message of step 3d.
|
||||
AuthRequired,
|
||||
/// Expecting the contact-confirm message.
|
||||
///
|
||||
/// This corresponds to the `vc-contact-confirm` or `vg-member-added` message of step
|
||||
/// 6b.
|
||||
ContactConfirm,
|
||||
}
|
||||
|
||||
impl Default for SecureJoinStep {
|
||||
fn default() -> Self {
|
||||
Self::NotActive
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<String> {
|
||||
/*=======================================================
|
||||
==== Alice - the inviter side ====
|
||||
@@ -159,8 +211,8 @@ async fn cleanup(
|
||||
join_vg: bool,
|
||||
) -> ChatId {
|
||||
let mut bob = context.bob.write().await;
|
||||
bob.expects = 0;
|
||||
let ret_chat_id: ChatId = if bob.status == DC_BOB_SUCCESS {
|
||||
bob.expects = SecureJoinStep::NotActive;
|
||||
let ret_chat_id: ChatId = if bob.status == BobStatus::Success {
|
||||
if join_vg {
|
||||
chat::get_chat_id_by_grpid(
|
||||
context,
|
||||
@@ -223,7 +275,7 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
|
||||
{
|
||||
let mut bob = context.bob.write().await;
|
||||
bob.status = 0;
|
||||
bob.status = BobStatus::Error;
|
||||
bob.qr_scan = Some(qr_scan);
|
||||
}
|
||||
if fingerprint_equals_sender(
|
||||
@@ -245,7 +297,7 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
// the scanned fingerprint matches Alice's key,
|
||||
// we can proceed to step 4b) directly and save two mails
|
||||
info!(context, "Taking protocol shortcut.");
|
||||
context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM;
|
||||
context.bob.write().await.expects = SecureJoinStep::ContactConfirm;
|
||||
joiner_progress!(
|
||||
context,
|
||||
chat_id_2_contact_id(context, contact_chat_id).await,
|
||||
@@ -276,7 +328,7 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
} else {
|
||||
context.bob.write().await.expects = DC_VC_AUTH_REQUIRED;
|
||||
context.bob.write().await.expects = SecureJoinStep::AuthRequired;
|
||||
|
||||
// Bob -> Alice
|
||||
if let Err(err) = send_handshake_msg(
|
||||
@@ -366,7 +418,20 @@ async fn fingerprint_equals_sender(
|
||||
) -> bool {
|
||||
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
|
||||
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
|
||||
if let Some(peerstate) = Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
|
||||
Ok(peerstate) => peerstate,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to sender peerstate for {}: {}",
|
||||
contact.get_addr(),
|
||||
err
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.public_key_fingerprint.is_some()
|
||||
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
|
||||
{
|
||||
@@ -508,7 +573,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
let bob = context.bob.read().await;
|
||||
let scan = bob.qr_scan.as_ref();
|
||||
scan.is_none()
|
||||
|| bob.expects != DC_VC_AUTH_REQUIRED
|
||||
|| bob.expects != SecureJoinStep::AuthRequired
|
||||
|| join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup
|
||||
};
|
||||
|
||||
@@ -532,7 +597,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
},
|
||||
)
|
||||
.await;
|
||||
context.bob.write().await.status = 0; // secure-join failed
|
||||
context.bob.write().await.status = BobStatus::Error; // secure-join failed
|
||||
context.stop_ongoing().await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
@@ -545,14 +610,14 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
"Fingerprint mismatch on joiner-side.",
|
||||
)
|
||||
.await;
|
||||
context.bob.write().await.status = 0; // secure-join failed
|
||||
context.bob.write().await.status = BobStatus::Error; // secure-join failed
|
||||
context.stop_ongoing().await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
info!(context, "Fingerprint verified.",);
|
||||
let own_fingerprint = get_self_fingerprint(context).await.unwrap();
|
||||
joiner_progress!(context, contact_id, 400);
|
||||
context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM;
|
||||
context.bob.write().await.expects = SecureJoinStep::ContactConfirm;
|
||||
|
||||
// Bob -> Alice
|
||||
send_handshake_msg(
|
||||
@@ -640,7 +705,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await;
|
||||
info!(context, "Auth verified.",);
|
||||
secure_connection_established(context, contact_chat_id).await;
|
||||
emit_event!(context, Event::ContactsChanged(Some(contact_id)));
|
||||
emit_event!(context, EventType::ContactsChanged(Some(contact_id)));
|
||||
inviter_progress!(context, contact_id, 600);
|
||||
if join_vg {
|
||||
// the vg-member-added message is special:
|
||||
@@ -696,7 +761,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
HandshakeMessage::Ignore
|
||||
};
|
||||
|
||||
if context.bob.read().await.expects != DC_VC_CONTACT_CONFIRM {
|
||||
if context.bob.read().await.expects != SecureJoinStep::ContactConfirm {
|
||||
info!(context, "Message belongs to a different handshake.",);
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
@@ -743,7 +808,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
"Contact confirm message not encrypted.",
|
||||
)
|
||||
.await;
|
||||
context.bob.write().await.status = 0;
|
||||
context.bob.write().await.status = BobStatus::Error;
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
|
||||
@@ -760,7 +825,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined).await;
|
||||
emit_event!(context, Event::ContactsChanged(None));
|
||||
emit_event!(context, EventType::ContactsChanged(None));
|
||||
let cg_member_added = mime_message
|
||||
.get(HeaderDef::ChatGroupMemberAdded)
|
||||
.map(|s| s.as_str())
|
||||
@@ -775,7 +840,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
secure_connection_established(context, contact_chat_id).await;
|
||||
context.bob.write().await.expects = 0;
|
||||
context.bob.write().await.expects = SecureJoinStep::NotActive;
|
||||
|
||||
// Bob -> Alice
|
||||
send_handshake_msg(
|
||||
@@ -792,7 +857,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.bob.write().await.status = 1;
|
||||
context.bob.write().await.status = BobStatus::Success;
|
||||
context.stop_ongoing().await;
|
||||
Ok(if join_vg {
|
||||
HandshakeMessage::Propagate
|
||||
@@ -947,7 +1012,7 @@ async fn secure_connection_established(context: &Context, contact_chat_id: ChatI
|
||||
.stock_string_repl_str(StockMessage::ContactVerified, addr)
|
||||
.await;
|
||||
chat::add_info_msg(context, contact_chat_id, msg).await;
|
||||
emit_event!(context, Event::ChatModified(contact_chat_id));
|
||||
emit_event!(context, EventType::ChatModified(contact_chat_id));
|
||||
}
|
||||
|
||||
async fn could_not_establish_secure_connection(
|
||||
@@ -974,7 +1039,7 @@ async fn could_not_establish_secure_connection(
|
||||
|
||||
async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> {
|
||||
if let Some(ref mut peerstate) =
|
||||
Peerstate::from_fingerprint(context, &context.sql, fingerprint).await
|
||||
Peerstate::from_fingerprint(context, &context.sql, fingerprint).await?
|
||||
{
|
||||
if peerstate.set_verified(
|
||||
PeerstateKeyType::PublicKey,
|
||||
@@ -1023,45 +1088,3 @@ fn encrypted_and_signed(
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_degrade_event(
|
||||
context: &Context,
|
||||
peerstate: &Peerstate<'_>,
|
||||
) -> Result<(), Error> {
|
||||
// - we do not issue an warning for DC_DE_ENCRYPTION_PAUSED as this is quite normal
|
||||
// - currently, we do not issue an extra warning for DC_DE_VERIFICATION_LOST - this always comes
|
||||
// together with DC_DE_FINGERPRINT_CHANGED which is logged, the idea is not to bother
|
||||
// with things they cannot fix, so the user is just kicked from the verified group
|
||||
// (and he will know this and can fix this)
|
||||
if Some(DegradeEvent::FingerprintChanged) == peerstate.degrade_event {
|
||||
let contact_id: i32 = match context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT id FROM contacts WHERE addr=?;",
|
||||
paramsv![peerstate.addr],
|
||||
)
|
||||
.await
|
||||
{
|
||||
None => bail!(
|
||||
"contact with peerstate.addr {:?} not found",
|
||||
&peerstate.addr
|
||||
),
|
||||
Some(contact_id) => contact_id,
|
||||
};
|
||||
if contact_id > 0 {
|
||||
let (contact_chat_id, _) =
|
||||
chat::create_or_lookup_by_contact_id(context, contact_id as u32, Blocked::Deaddrop)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let msg = context
|
||||
.stock_string_repl_str(StockMessage::ContactSetupChanged, peerstate.addr.clone())
|
||||
.await;
|
||||
|
||||
chat::add_info_msg(context, contact_chat_id, msg).await;
|
||||
emit_event!(context, Event::ChatModified(contact_chat_id));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -46,9 +46,7 @@ fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
|
||||
fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
if line == "--"
|
||||
|| line == "---"
|
||||
|| line == "----"
|
||||
|| line.starts_with("-----")
|
||||
|| line.starts_with("---")
|
||||
|| line.starts_with("_____")
|
||||
|| line.starts_with("=====")
|
||||
|| line.starts_with("*****")
|
||||
@@ -336,9 +334,17 @@ mod tests {
|
||||
let (plain, _) = simplify(escaped, true);
|
||||
assert_eq!(plain, "text\n\n--\ntreated as footer when unescaped");
|
||||
|
||||
// Nonstandard footer sent by https://siju.es/
|
||||
let input = "Message text here\n---Desde mi teléfono con SIJÚ\n\nQuote here".to_string();
|
||||
let (plain, _) = simplify(input.clone(), false);
|
||||
assert_eq!(plain, "Message text here [...]");
|
||||
let (plain, _) = simplify(input.clone(), true);
|
||||
assert_eq!(plain, input);
|
||||
|
||||
let input = "--\ntreated as footer when unescaped".to_string();
|
||||
let (plain, _) = simplify(input.clone(), true);
|
||||
assert_eq!(plain, ""); // see remove_message_footer() for some explanations
|
||||
|
||||
let escaped = escape_message_footer_marks(&input);
|
||||
let (plain, _) = simplify(escaped, true);
|
||||
assert_eq!(plain, "--\ntreated as footer when unescaped");
|
||||
|
||||
@@ -9,10 +9,10 @@ use async_smtp::*;
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
use crate::login_param::{dc_build_tls, CertificateChecks, LoginParam};
|
||||
use crate::events::EventType;
|
||||
use crate::login_param::{dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam};
|
||||
use crate::oauth2::*;
|
||||
use crate::provider::get_provider_info;
|
||||
use crate::provider::{get_provider_info, Socket};
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
/// SMTP write and read timeout in seconds.
|
||||
@@ -94,31 +94,53 @@ impl Smtp {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Connect using configured parameters.
|
||||
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
|
||||
if self.is_connected().await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let lp = LoginParam::from_database(context, "configured_").await;
|
||||
self.connect(
|
||||
context,
|
||||
&lp.smtp,
|
||||
&lp.addr,
|
||||
lp.server_flags & DC_LP_AUTH_OAUTH2 != 0,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Connect using the provided login params.
|
||||
pub async fn connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> {
|
||||
pub async fn connect(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
lp: &ServerLoginParam,
|
||||
addr: &str,
|
||||
oauth2: bool,
|
||||
) -> Result<()> {
|
||||
if self.is_connected().await {
|
||||
warn!(context, "SMTP already connected.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if lp.send_server.is_empty() || lp.send_port == 0 {
|
||||
context.emit_event(Event::ErrorNetwork("SMTP bad parameters.".into()));
|
||||
if lp.server.is_empty() || lp.port == 0 {
|
||||
context.emit_event(EventType::ErrorNetwork("SMTP bad parameters.".into()));
|
||||
return Err(Error::BadParameters);
|
||||
}
|
||||
|
||||
let from =
|
||||
EmailAddress::new(lp.addr.clone()).map_err(|err| Error::InvalidLoginAddress {
|
||||
address: lp.addr.clone(),
|
||||
EmailAddress::new(addr.to_string()).map_err(|err| Error::InvalidLoginAddress {
|
||||
address: addr.to_string(),
|
||||
error: err,
|
||||
})?;
|
||||
|
||||
self.from = Some(from);
|
||||
|
||||
let domain = &lp.send_server;
|
||||
let port = lp.send_port as u16;
|
||||
let domain = &lp.server;
|
||||
let port = lp.port;
|
||||
|
||||
let provider = get_provider_info(&lp.addr);
|
||||
let strict_tls = match lp.smtp_certificate_checks {
|
||||
let provider = get_provider_info(addr);
|
||||
let strict_tls = match lp.certificate_checks {
|
||||
CertificateChecks::Automatic => provider.map_or(false, |provider| provider.strict_tls),
|
||||
CertificateChecks::Strict => true,
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
@@ -127,17 +149,16 @@ impl Smtp {
|
||||
let tls_config = dc_build_tls(strict_tls);
|
||||
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls_config);
|
||||
|
||||
let (creds, mechanism) = if 0 != lp.server_flags & (DC_LP_AUTH_OAUTH2 as i32) {
|
||||
let (creds, mechanism) = if oauth2 {
|
||||
// oauth2
|
||||
let addr = &lp.addr;
|
||||
let send_pw = &lp.send_pw;
|
||||
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::Oauth2Error {
|
||||
address: addr.to_string(),
|
||||
});
|
||||
}
|
||||
let user = &lp.send_user;
|
||||
let user = &lp.user;
|
||||
(
|
||||
smtp::authentication::Credentials::new(
|
||||
user.to_string(),
|
||||
@@ -147,8 +168,8 @@ impl Smtp {
|
||||
)
|
||||
} else {
|
||||
// plain
|
||||
let user = lp.send_user.clone();
|
||||
let pw = lp.send_pw.clone();
|
||||
let user = lp.user.clone();
|
||||
let pw = lp.password.clone();
|
||||
(
|
||||
smtp::authentication::Credentials::new(user, pw),
|
||||
vec![
|
||||
@@ -158,12 +179,9 @@ impl Smtp {
|
||||
)
|
||||
};
|
||||
|
||||
let security = if 0
|
||||
!= lp.server_flags & (DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_PLAIN) as i32
|
||||
{
|
||||
smtp::ClientSecurity::Opportunistic(tls_parameters)
|
||||
} else {
|
||||
smtp::ClientSecurity::Wrapper(tls_parameters)
|
||||
let security = match lp.security {
|
||||
Socket::STARTTLS | Socket::Plain => smtp::ClientSecurity::Opportunistic(tls_parameters),
|
||||
_ => smtp::ClientSecurity::Wrapper(tls_parameters),
|
||||
};
|
||||
|
||||
let client = smtp::SmtpClient::with_security((domain.as_str(), port), security)
|
||||
@@ -187,16 +205,16 @@ impl Smtp {
|
||||
)
|
||||
.await;
|
||||
|
||||
emit_event!(context, Event::ErrorNetwork(message));
|
||||
emit_event!(context, EventType::ErrorNetwork(message));
|
||||
return Err(Error::ConnectionFailure(err));
|
||||
}
|
||||
|
||||
self.transport = Some(trans);
|
||||
self.last_success = Some(SystemTime::now());
|
||||
|
||||
context.emit_event(Event::SmtpConnected(format!(
|
||||
context.emit_event(EventType::SmtpConnected(format!(
|
||||
"SMTP-LOGIN as {} ok",
|
||||
lp.send_user,
|
||||
lp.user,
|
||||
)));
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::Smtp;
|
||||
use async_smtp::*;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
use crate::events::EventType;
|
||||
use std::time::Duration;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -55,7 +55,7 @@ impl Smtp {
|
||||
.await
|
||||
.map_err(Error::SendError)?;
|
||||
|
||||
context.emit_event(Event::SmtpMessageSent(format!(
|
||||
context.emit_event(EventType::SmtpMessageSent(format!(
|
||||
"Message len={} was smtp-sent to {}",
|
||||
message_len_bytes, recipients_display
|
||||
)));
|
||||
|
||||
44
src/sql.rs
@@ -1283,6 +1283,48 @@ async fn open(
|
||||
.await?;
|
||||
sql.set_raw_config_int(context, "dbversion", 65).await?;
|
||||
}
|
||||
if dbversion < 66 {
|
||||
info!(context, "[migration] v66");
|
||||
update_icons = true;
|
||||
sql.set_raw_config_int(context, "dbversion", 66).await?;
|
||||
}
|
||||
if dbversion < 67 {
|
||||
info!(context, "[migration] v67");
|
||||
for prefix in &["", "configured_"] {
|
||||
if let Some(server_flags) = sql
|
||||
.get_raw_config_int(context, format!("{}server_flags", prefix))
|
||||
.await
|
||||
{
|
||||
let imap_socket_flags = server_flags & 0x700;
|
||||
let key = format!("{}mail_security", prefix);
|
||||
match imap_socket_flags {
|
||||
0x100 => sql.set_raw_config_int(context, key, 2).await?, // STARTTLS
|
||||
0x200 => sql.set_raw_config_int(context, key, 1).await?, // SSL/TLS
|
||||
0x400 => sql.set_raw_config_int(context, key, 3).await?, // Plain
|
||||
_ => sql.set_raw_config_int(context, key, 0).await?,
|
||||
}
|
||||
let smtp_socket_flags = server_flags & 0x70000;
|
||||
let key = format!("{}send_security", prefix);
|
||||
match smtp_socket_flags {
|
||||
0x10000 => sql.set_raw_config_int(context, key, 2).await?, // STARTTLS
|
||||
0x20000 => sql.set_raw_config_int(context, key, 1).await?, // SSL/TLS
|
||||
0x40000 => sql.set_raw_config_int(context, key, 3).await?, // Plain
|
||||
_ => sql.set_raw_config_int(context, key, 0).await?,
|
||||
}
|
||||
}
|
||||
}
|
||||
sql.set_raw_config_int(context, "dbversion", 67).await?;
|
||||
}
|
||||
if dbversion < 68 {
|
||||
info!(context, "[migration] v68");
|
||||
// the index is used to speed up get_fresh_msg_cnt(), see comment there for more details
|
||||
sql.execute(
|
||||
"CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
sql.set_raw_config_int(context, "dbversion", 68).await?;
|
||||
}
|
||||
|
||||
// (2) updates that require high-level objects
|
||||
// (the structure is complete now and all objects are usable)
|
||||
@@ -1303,7 +1345,7 @@ async fn open(
|
||||
)
|
||||
.await?;
|
||||
for addr in &addrs {
|
||||
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await {
|
||||
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
|
||||
peerstate.recalc_fingerprint();
|
||||
peerstate.save_to_db(sql, false).await?;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,14 @@ impl TestContext {
|
||||
///
|
||||
/// [Context]: crate::context::Context
|
||||
pub async fn new() -> Self {
|
||||
use rand::Rng;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let ctx = Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
|
||||
let id = rand::thread_rng().gen();
|
||||
let ctx = Context::new("FakeOS".into(), dbfile.into(), id)
|
||||
.await
|
||||
.unwrap();
|
||||
Self { ctx, dir }
|
||||
}
|
||||
|
||||
|
||||
59
test-data/autoconfig/lakenet.ch.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="lakenet.ch">
|
||||
<domain>%EMAILDOMAIN%</domain>
|
||||
<displayName>%EMAILDOMAIN% Mail</displayName>
|
||||
<displayShortName>%EMAILDOMAIN%</displayShortName>
|
||||
<incomingServer type="imap">
|
||||
<hostname>mail.lakenet.ch</hostname>
|
||||
<port>993</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
<incomingServer type="imap">
|
||||
<hostname>mail.lakenet.ch</hostname>
|
||||
<port>143</port>
|
||||
<socketType>STARTTLS</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
<incomingServer type="pop3">
|
||||
<hostname>mail.lakenet.ch</hostname>
|
||||
<port>995</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
<incomingServer type="pop3">
|
||||
<hostname>mail.lakenet.ch</hostname>
|
||||
<port>110</port>
|
||||
<socketType>STARTTLS</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
<outgoingServer type="smtp">
|
||||
<hostname>mail.lakenet.ch</hostname>
|
||||
<port>587</port>
|
||||
<socketType>STARTTLS</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</outgoingServer>
|
||||
<documentation url="https://www.lakenet.ch/">
|
||||
<descr lang="it">Impostazioni per le e-mail LakeNet</descr>
|
||||
<descr lang="fr">Reglages pour le courriel e-mail LakeNet</descr>
|
||||
<descr lang="en">Settings for LakeNet's e-mail accounts</descr>
|
||||
</documentation>
|
||||
</emailProvider>
|
||||
<webMail>
|
||||
<loginPage url="https://lakenet.ch/webmail/" />
|
||||
<loginPageInfo url="https://lakenet.ch/webmail/">
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<usernameField id="rcmloginuser" name="_user"/>
|
||||
<passwordField id="rcmloginpwd" name="_pass"/>
|
||||
<loginButton id="rcmloginsubmit"/>
|
||||
</loginPageInfo>
|
||||
</webMail>
|
||||
<clientConfigUpdate url="https://lakenet.ch/.well-known/autoconfig/mail/config-v1.1.xml" />
|
||||
</clientConfig>
|
||||
71
test-data/autoconfig/outlook.com.xml
Normal file
@@ -0,0 +1,71 @@
|
||||
<clientConfig version="1.1">
|
||||
<!-- Retrieved from https://autoconfig.thunderbird.net/v1.1/outlook.com on 2019-10-11 -->
|
||||
<emailProvider id="outlook.com">
|
||||
<domain>hotmail.com</domain>
|
||||
<domain>hotmail.co.uk</domain>
|
||||
<domain>hotmail.co.jp</domain>
|
||||
<domain>hotmail.com.br</domain>
|
||||
<domain>hotmail.de</domain>
|
||||
<domain>hotmail.fr</domain>
|
||||
<domain>hotmail.it</domain>
|
||||
<domain>hotmail.es</domain>
|
||||
<domain>live.com</domain>
|
||||
<domain>live.co.uk</domain>
|
||||
<domain>live.co.jp</domain>
|
||||
<domain>live.de</domain>
|
||||
<domain>live.fr</domain>
|
||||
<domain>live.it</domain>
|
||||
<domain>live.jp</domain>
|
||||
<domain>msn.com</domain>
|
||||
<domain>outlook.com</domain>
|
||||
<displayName>Outlook.com (Microsoft)</displayName>
|
||||
<displayShortName>Outlook</displayShortName>
|
||||
<incomingServer type="exchange">
|
||||
<hostname>outlook.office365.com</hostname>
|
||||
<port>443</port>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>OAuth2</authentication>
|
||||
<owaURL>https://outlook.office365.com/owa/</owaURL>
|
||||
<ewsURL>https://outlook.office365.com/ews/exchange.asmx</ewsURL>
|
||||
<useGlobalPreferredServer>true</useGlobalPreferredServer>
|
||||
</incomingServer>
|
||||
<incomingServer type="imap">
|
||||
<hostname>outlook.office365.com</hostname>
|
||||
<port>993</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
<incomingServer type="pop3">
|
||||
<hostname>outlook.office365.com</hostname>
|
||||
<port>995</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<pop3>
|
||||
<leaveMessagesOnServer>true</leaveMessagesOnServer>
|
||||
<!-- Outlook.com docs specifically mention that POP3 deletes have effect on the main inbox on webmail and IMAP -->
|
||||
</pop3>
|
||||
</incomingServer>
|
||||
<outgoingServer type="smtp">
|
||||
<hostname>smtp.office365.com</hostname>
|
||||
<port>587</port>
|
||||
<socketType>STARTTLS</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</outgoingServer>
|
||||
<documentation url="http://windows.microsoft.com/en-US/windows/outlook/send-receive-from-app">
|
||||
<descr lang="en">Set up an email app with Outlook.com</descr>
|
||||
</documentation>
|
||||
</emailProvider>
|
||||
<webMail>
|
||||
<loginPage url="https://www.outlook.com/"/>
|
||||
<loginPageInfo url="https://www.outlook.com/">
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<usernameField id="i0116" name="login"/>
|
||||
<passwordField id="i0118" name="passwd"/>
|
||||
<loginButton id="idSIButton9" name="SI"/>
|
||||
</loginPageInfo>
|
||||
</webMail>
|
||||
</clientConfig>";
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
BIN
test-data/image/image100x50.gif
Normal file
|
After Width: | Height: | Size: 274 B |
114
test-data/message/wrong-html.eml
Normal file
@@ -0,0 +1,114 @@
|
||||
Return-Path: <camping@otherdomain.eu>
|
||||
X-Original-To: alice@example.com
|
||||
Delivered-To: m045a7e8@dd37930.kasserver.com
|
||||
Received: from mout.kundenserver.de (mout.kundenserver.de [212.227.126.131])
|
||||
by dd37930.kasserver.com (Postfix) with ESMTPS id 271F34B4258C
|
||||
for <holger@somedomain.de>; Thu, 6 Aug 2020 18:40:32 +0200 (CEST)
|
||||
Received: from oxbsltgw18.schlund.de ([172.19.249.35]) by
|
||||
mrelayeu.kundenserver.de (mreue009 [213.165.67.103]) with ESMTPSA (Nemesis)
|
||||
id 1MpDRv-1kW93Y0lGZ-00qjvh for <holger@somedomain.de>; Thu, 06 Aug 2020
|
||||
18:40:31 +0200
|
||||
Date: Thu, 6 Aug 2020 18:40:30 +0200 (CEST)
|
||||
From: Camping <camping@otherdomain.eu>
|
||||
Reply-To: Camping <camping@otherdomain.eu>
|
||||
To: Alice <alice@example.com>
|
||||
Message-ID: <512278196.1287440.1596732031020@email.ionos.fr>
|
||||
Subject: Re: subj?
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/related;
|
||||
boundary="----=_Part_1287438_2124736777.1596732031007"
|
||||
X-Mailer: Open-Xchange Mailer v7.10.1-Rev32
|
||||
X-Originating-Client: open-xchange-appsuite
|
||||
|
||||
------=_Part_1287438_2124736777.1596732031007
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>=20
|
||||
<meta charset=3D"UTF-8">=20
|
||||
</head>
|
||||
<body>
|
||||
<div class=3D"default-style">
|
||||
Guten Abend,
|
||||
<br>
|
||||
</div>
|
||||
<div class=3D"default-style">
|
||||
<br>
|
||||
</div>
|
||||
<div class=3D"default-style">
|
||||
Lots of text
|
||||
<br>
|
||||
</div>
|
||||
<div class=3D"default-style">
|
||||
text with Umlaut =C3=A4...
|
||||
<br>
|
||||
</div>
|
||||
<div class=3D"default-style">
|
||||
MfG
|
||||
<br>
|
||||
</div>
|
||||
<div class=3D"default-style">
|
||||
<br>
|
||||
</div>
|
||||
<div class=3D"io-ox-signature">
|
||||
<p>--------------------------------------<br></p>
|
||||
<p style=3D"text-align: left;"><a href=3D"https://example.com=
|
||||
/">Camping </a><br></p>
|
||||
<p>someaddress<br></p>
|
||||
<p>sometown</p>
|
||||
<p><img alt=3D"" class=3D"aspect-ratio" style=3D"width: 505px; height: 1=
|
||||
68px; max-width: 100%;" id=3D"d5cd260f-1b1f-4bfa-81d7-c0a74923b979" src=3D"=
|
||||
cid:d5cd260f-1b1f-4bfa-81d7-c0a74923b979" width=3D"505" height=3D"168"><br>=
|
||||
</p>
|
||||
</div>
|
||||
<blockquote type=3D"cite">
|
||||
<div>
|
||||
Le 5 ao=C3=BBt 2020 =C3=A0 10:46, holger <
|
||||
<a href=3D"mailto:holger@somedomain.de">holger@somedomain.de</a>> a =
|
||||
=C3=A9crit :
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
<div>
|
||||
Bonjour,
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
<div>
|
||||
--
|
||||
</div>
|
||||
<div>
|
||||
Sent with my Delta Chat Messenger:=20
|
||||
<a href=3D"https://delta.chat" rel=3D"noopener" target=3D"_blank">https=
|
||||
://delta.chat</a>
|
||||
<br>
|
||||
</div>
|
||||
</blockquote>=20
|
||||
</body>
|
||||
</html>
|
||||
------=_Part_1287438_2124736777.1596732031007
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <d5cd260f-1b1f-4bfa-81d7-c0a74923b979>
|
||||
Content-Disposition: inline
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAAlgAAADICAYAAAA0n5+2AAAgAElEQVR4nCzaZ3sjhIGo7fyMPSch
|
||||
MN29S7YsW71avfcuWZJlS3K33HuvY08fpjEVhoGhDh0CSSAESEhCgDRSKNns5mx2k8AM5Dznw/v+
|
||||
[scrubbed]
|
||||
/kNZ/B9Uub32fzngSwAAAABJRU5ErkJggg==
|
||||
------=_Part_1287438_2124736777.1596732031007--
|
||||
|
||||
@@ -99,9 +99,14 @@ struct TestContext {
|
||||
}
|
||||
|
||||
async fn create_test_context() -> TestContext {
|
||||
use rand::Rng;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let ctx = Context::new("FakeOs".into(), dbfile.into()).await.unwrap();
|
||||
let id = rand::thread_rng().gen();
|
||||
let ctx = Context::new("FakeOs".into(), dbfile.into(), id)
|
||||
.await
|
||||
.unwrap();
|
||||
TestContext { ctx, dir }
|
||||
}
|
||||
|
||||
|
||||