mirror of
https://github.com/chatmail/core.git
synced 2026-04-13 03:30:29 +03:00
Compare commits
381 Commits
py-0.700.0
...
export_cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6161b50bc7 | ||
|
|
cbd45bc0ea | ||
|
|
b3eaf6730f | ||
|
|
c3be1e3163 | ||
|
|
2e60380803 | ||
|
|
108826d4af | ||
|
|
d36d6bc87c | ||
|
|
7b5c946c82 | ||
|
|
8665a3f8ad | ||
|
|
c99131f551 | ||
|
|
9f24d57835 | ||
|
|
817050260f | ||
|
|
e3b8b64c16 | ||
|
|
d31265895d | ||
|
|
6e35a879a3 | ||
|
|
9c2a3b8a82 | ||
|
|
916fab7d4b | ||
|
|
3163ef87c6 | ||
|
|
1934181b52 | ||
|
|
1b815a7d96 | ||
|
|
4e0a08106d | ||
|
|
e8cc739fbd | ||
|
|
fc57cbfb49 | ||
|
|
7522fec044 | ||
|
|
237dabb907 | ||
|
|
3686048ab6 | ||
|
|
2bf4c5d7e7 | ||
|
|
051d80b2f3 | ||
|
|
adaa1e856c | ||
|
|
4daa57c98e | ||
|
|
7e67b2cbb3 | ||
|
|
270d18a88a | ||
|
|
25f8a735a9 | ||
|
|
9eb672ea17 | ||
|
|
9febc762da | ||
|
|
4b742c220c | ||
|
|
9d03d441e1 | ||
|
|
ff8b249cc6 | ||
|
|
248e6ea5e7 | ||
|
|
be0afdebfd | ||
|
|
9f19d20344 | ||
|
|
aea8a32ba5 | ||
|
|
d1a4c82937 | ||
|
|
4f73812673 | ||
|
|
33150615a1 | ||
|
|
491f83c86d | ||
|
|
41f776763b | ||
|
|
65fdfac866 | ||
|
|
cb0c00bc6d | ||
|
|
ad53678c19 | ||
|
|
62097765a6 | ||
|
|
efb7280e99 | ||
|
|
bdb2a47743 | ||
|
|
c4677190be | ||
|
|
055aba189c | ||
|
|
314c3d5e78 | ||
|
|
6db03356b5 | ||
|
|
28af919b09 | ||
|
|
8f7a456a39 | ||
|
|
5b3bec1aac | ||
|
|
f2aa17c9d0 | ||
|
|
bc06b9e051 | ||
|
|
6d216af507 | ||
|
|
b2f1d9f376 | ||
|
|
a653e469f2 | ||
|
|
4f4241ba3a | ||
|
|
2cf9c68040 | ||
|
|
cc0f977d6f | ||
|
|
7879952fde | ||
|
|
4452cab987 | ||
|
|
98bd64621a | ||
|
|
c1c769ceb0 | ||
|
|
d64e55c66f | ||
|
|
76fc84be37 | ||
|
|
8cd5f5990e | ||
|
|
6ffe54d68f | ||
|
|
d78ea882c8 | ||
|
|
958802a233 | ||
|
|
00b02efdc2 | ||
|
|
50569f12f5 | ||
|
|
8aa4ceb570 | ||
|
|
a7afbf85ad | ||
|
|
8fdf3dcdb8 | ||
|
|
818c20e0cb | ||
|
|
c1d4996777 | ||
|
|
fd3e6e0ee4 | ||
|
|
edc5754c68 | ||
|
|
bd75dea000 | ||
|
|
e09a0a548f | ||
|
|
15ee8b4362 | ||
|
|
ab8d75b192 | ||
|
|
e135c969c9 | ||
|
|
36e7090466 | ||
|
|
f28f177c6b | ||
|
|
785973c624 | ||
|
|
9c06acff72 | ||
|
|
4fabddeb47 | ||
|
|
17ff1ab372 | ||
|
|
3c34096392 | ||
|
|
70e0d3b571 | ||
|
|
ae5a2396f3 | ||
|
|
8f82bf40e0 | ||
|
|
fe398de2fa | ||
|
|
a770d75e2e | ||
|
|
a330104e9b | ||
|
|
aae3cae4bb | ||
|
|
e7e4821804 | ||
|
|
9654802acc | ||
|
|
06a24fa4d0 | ||
|
|
62b1b0519a | ||
|
|
10afdfecdd | ||
|
|
c0e08fb927 | ||
|
|
6d6bc9b050 | ||
|
|
4714fb6887 | ||
|
|
5f47810964 | ||
|
|
0f6024e055 | ||
|
|
fafc15f80c | ||
|
|
9a85ea861d | ||
|
|
9541960307 | ||
|
|
95073deb96 | ||
|
|
82b4647b95 | ||
|
|
0c770a8b37 | ||
|
|
0e4031348f | ||
|
|
5cc26762c2 | ||
|
|
b8b4853d1f | ||
|
|
fbabe27fc1 | ||
|
|
4d1554c85b | ||
|
|
ab40495d5c | ||
|
|
42ebf49f92 | ||
|
|
c5ccd88f79 | ||
|
|
dbd1b227d9 | ||
|
|
63baac3c61 | ||
|
|
7c39bb6659 | ||
|
|
4f8c5965ac | ||
|
|
900a17fc00 | ||
|
|
78f36aaa0d | ||
|
|
e064e02794 | ||
|
|
e22e5045f1 | ||
|
|
087f35482b | ||
|
|
23ff5fea28 | ||
|
|
34347ccaf5 | ||
|
|
e704eb6cef | ||
|
|
bf63423fec | ||
|
|
f6d71ed8ef | ||
|
|
3c342339a1 | ||
|
|
33463856c5 | ||
|
|
a18f4c9b1b | ||
|
|
783c7ee4c5 | ||
|
|
a0b2a692d0 | ||
|
|
a59d368101 | ||
|
|
5c36fb29ed | ||
|
|
508b8ef2e2 | ||
|
|
e94c62e5b3 | ||
|
|
b65a6c2829 | ||
|
|
c4a20d0798 | ||
|
|
9cb7ea524e | ||
|
|
0ac0eeda34 | ||
|
|
4d066b4fd2 | ||
|
|
840e321dd9 | ||
|
|
4b6963122b | ||
|
|
d5d662bc41 | ||
|
|
0b0ed56901 | ||
|
|
13e361aabc | ||
|
|
d1a26e66a7 | ||
|
|
ffe3c84e7c | ||
|
|
702c7382a7 | ||
|
|
b138d486e4 | ||
|
|
3a25d6b275 | ||
|
|
66e2f51233 | ||
|
|
8fdb048b6a | ||
|
|
fa3d98a492 | ||
|
|
d9dda44409 | ||
|
|
7368c01a8f | ||
|
|
21ac5be7ca | ||
|
|
e14a113277 | ||
|
|
66d3440675 | ||
|
|
6b6be3b03d | ||
|
|
cda8158bec | ||
|
|
332e0dc4a8 | ||
|
|
f7b4c6837b | ||
|
|
531928bf0b | ||
|
|
490c8e055b | ||
|
|
bcbf192bbc | ||
|
|
78d855c5ca | ||
|
|
1fa9aa88a8 | ||
|
|
08c77c2668 | ||
|
|
793ebe1b0f | ||
|
|
4c42acc7e1 | ||
|
|
4eb9660bfa | ||
|
|
8ed08f701d | ||
|
|
784964efad | ||
|
|
adb96e72b9 | ||
|
|
439c6f7296 | ||
|
|
e2f1ea1444 | ||
|
|
2977ceb459 | ||
|
|
e00d4e0ed8 | ||
|
|
772127d9d8 | ||
|
|
6ba45c88ec | ||
|
|
5a4040cf0b | ||
|
|
b54f580e66 | ||
|
|
a9ac69fe9c | ||
|
|
5c52b5e404 | ||
|
|
b80360b7da | ||
|
|
2753883687 | ||
|
|
ced73ffb14 | ||
|
|
672fe2dfd7 | ||
|
|
04bb6997a2 | ||
|
|
c8a8dbbbae | ||
|
|
1f9520dc78 | ||
|
|
84f8627890 | ||
|
|
a177df32b7 | ||
|
|
f25d5dd123 | ||
|
|
4cfa9e6165 | ||
|
|
0303ea7f57 | ||
|
|
2813e01e61 | ||
|
|
e3420da60f | ||
|
|
60493d30f6 | ||
|
|
6efe8e7d7c | ||
|
|
2e8409f146 | ||
|
|
ac4b2b9dfe | ||
|
|
23b6178e78 | ||
|
|
5e5d45fb0a | ||
|
|
1765b8f2cf | ||
|
|
5678562ce2 | ||
|
|
7274197da0 | ||
|
|
c79fcb380b | ||
|
|
6a98eade07 | ||
|
|
9008a65c14 | ||
|
|
4e07e4c7f3 | ||
|
|
e440d8503a | ||
|
|
e9bacff830 | ||
|
|
9cc99ffcd6 | ||
|
|
beb91271de | ||
|
|
7e9585ebc5 | ||
|
|
0c4b3f71e5 | ||
|
|
5c17ec5f01 | ||
|
|
8b4edc46a7 | ||
|
|
2b7a0a4585 | ||
|
|
1882176489 | ||
|
|
875e89e71a | ||
|
|
52520635ea | ||
|
|
aa50a9ba83 | ||
|
|
489e5111ac | ||
|
|
2d4c20af35 | ||
|
|
66fdb447f7 | ||
|
|
c801775a39 | ||
|
|
f5bb57d6a6 | ||
|
|
1071ab05db | ||
|
|
8461cf6443 | ||
|
|
bb10501f56 | ||
|
|
c4d5f657da | ||
|
|
cabb58a9aa | ||
|
|
e64ce5bb4f | ||
|
|
bc750d61d2 | ||
|
|
3150901b6e | ||
|
|
4f6745b742 | ||
|
|
bcdc323a97 | ||
|
|
4fd4cc709d | ||
|
|
0fac463144 | ||
|
|
d809dfac65 | ||
|
|
983fd70260 | ||
|
|
ea11a5274e | ||
|
|
42356c2a67 | ||
|
|
5a84ab2011 | ||
|
|
b4573e341f | ||
|
|
df252c4704 | ||
|
|
d1912f873b | ||
|
|
e525c42c7d | ||
|
|
0242322d24 | ||
|
|
ded6fafc8a | ||
|
|
4aebd678c3 | ||
|
|
afc9ed2274 | ||
|
|
d73d021e3c | ||
|
|
c8fe81e21d | ||
|
|
621f1df913 | ||
|
|
5f4274b449 | ||
|
|
4acb37156f | ||
|
|
1ca23a7479 | ||
|
|
61daf7218d | ||
|
|
dc6671fc4e | ||
|
|
f34237ebc8 | ||
|
|
aadeb3b87e | ||
|
|
1fb75c1af3 | ||
|
|
e04d28c885 | ||
|
|
6d80b3675a | ||
|
|
07d698f8dc | ||
|
|
ef158504e7 | ||
|
|
63be1ae5a9 | ||
|
|
b9ba1a4f69 | ||
|
|
e006d9b033 | ||
|
|
1538684c6c | ||
|
|
b37e83caab | ||
|
|
d11d3ab08b | ||
|
|
1144a536a5 | ||
|
|
515c753d11 | ||
|
|
0864e640ed | ||
|
|
b876e49393 | ||
|
|
fc0292bf8a | ||
|
|
a903805cd9 | ||
|
|
abab34573e | ||
|
|
fa1b94af60 | ||
|
|
81ff5d1224 | ||
|
|
5c3a8819a4 | ||
|
|
4d0a08d858 | ||
|
|
c41bdaa2b7 | ||
|
|
4bf2fc18e5 | ||
|
|
145fd8657f | ||
|
|
01b55d1d29 | ||
|
|
98b3151c5f | ||
|
|
c7eca8deb3 | ||
|
|
627b54f712 | ||
|
|
8ef7b6fc54 | ||
|
|
d83652b0fc | ||
|
|
cce32229e0 | ||
|
|
24edd83c8a | ||
|
|
268c5b6482 | ||
|
|
a66a754126 | ||
|
|
18059734ce | ||
|
|
c883e709c3 | ||
|
|
7be0bd3583 | ||
|
|
d9ab37ea58 | ||
|
|
ff075ba612 | ||
|
|
9c9294a730 | ||
|
|
2a0842b8ae | ||
|
|
3cfe45ffc2 | ||
|
|
80dc7bfc52 | ||
|
|
10f26f17ba | ||
|
|
4ba7402f28 | ||
|
|
d4da2e0d9c | ||
|
|
b180d004ba | ||
|
|
de5bd96f08 | ||
|
|
3a05b5dacc | ||
|
|
b3c4e32b68 | ||
|
|
375a48f135 | ||
|
|
1750ab92e6 | ||
|
|
8364ddd10b | ||
|
|
63043cb45d | ||
|
|
aaa6497659 | ||
|
|
4fc6fa9c8a | ||
|
|
7d0dcfb3a5 | ||
|
|
a97ea0ad63 | ||
|
|
da66a4d22f | ||
|
|
748e54d4c2 | ||
|
|
0f172595d7 | ||
|
|
5ffdbd99e8 | ||
|
|
fbe57c4c71 | ||
|
|
61726168d6 | ||
|
|
a80632ab36 | ||
|
|
893eb8b73b | ||
|
|
07a5ee7d2c | ||
|
|
c7a300be2f | ||
|
|
ef842fca89 | ||
|
|
bdbe9e1ca5 | ||
|
|
0c7f65222c | ||
|
|
a8fa644d25 | ||
|
|
cf7ccb5b8c | ||
|
|
49fdd6fc5b | ||
|
|
5e91c74304 | ||
|
|
b83e6f6e7c | ||
|
|
2687777a82 | ||
|
|
918b8036ea | ||
|
|
7ea0e4d4db | ||
|
|
622c1705aa | ||
|
|
3a08929b05 | ||
|
|
99e30d561d | ||
|
|
033a44580c | ||
|
|
4734bcfbb4 | ||
|
|
099fe6f477 | ||
|
|
889327b5f6 | ||
|
|
a2845f44ab | ||
|
|
a7477516d1 | ||
|
|
938d5828fc | ||
|
|
bf3eab453c | ||
|
|
ebab893330 | ||
|
|
4c67b3a118 | ||
|
|
d74b06f8bf | ||
|
|
c54e211147 | ||
|
|
8817cf5116 | ||
|
|
fb568513b2 | ||
|
|
b4ebfdb84a | ||
|
|
7040bd804a |
1
.github/workflows/code-quality.yml
vendored
1
.github/workflows/code-quality.yml
vendored
@@ -15,6 +15,7 @@ jobs:
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --workspace --examples --tests --all-features
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
|
||||
88
CHANGELOG.md
88
CHANGELOG.md
@@ -1,5 +1,93 @@
|
||||
# Changelog
|
||||
|
||||
## 1.28.0
|
||||
|
||||
- new flag DC_GCL_FOR_FORWARDING for dc_get_chatlist()
|
||||
that will sort the "saved messages" chat to the top of the chatlist #1336
|
||||
- mark mails as being deleted from server in dc_empty_server() #1333
|
||||
- fix interaction with servers that do not allow folder creation on root-level;
|
||||
use path separator as defined by the email server #1359
|
||||
- fix group creation if group was created by non-delta clients #1357
|
||||
- fix showing replies from non-delta clients #1353
|
||||
- fix member list on rejoining left groups #1343
|
||||
- fix crash when using empty groups #1354
|
||||
- fix potential crash on special names #1350
|
||||
|
||||
|
||||
## 1.27.0
|
||||
|
||||
- handle keys reliably on armv7 #1327
|
||||
|
||||
|
||||
## 1.26.0
|
||||
|
||||
- change generated key type back to RSA as shipped versions
|
||||
have problems to encrypt to Ed25519 keys
|
||||
|
||||
- update rPGP to encrypt reliably to Ed25519 keys;
|
||||
one of the next versions can finally use Ed25519 keys then
|
||||
|
||||
|
||||
## 1.25.0
|
||||
|
||||
- save traffic by downloading only messages that are really displayed #1236
|
||||
|
||||
- change generated key type to Ed25519, these keys are much shorter
|
||||
than RSA keys, which results in saving traffic and speed improvements #1287
|
||||
|
||||
- improve key handling #1237 #1240 #1242 #1247
|
||||
|
||||
- mute handling, apis are dc_set_chat_mute_duration()
|
||||
dc_chat_is_muted() and dc_chat_get_remaining_mute_duration() #1143
|
||||
|
||||
- pinning chats, new apis are dc_set_chat_visibility() and
|
||||
dc_chat_get_visibility() #1248
|
||||
|
||||
- add dc_provider_new_from_email() api that queries the new, integrated
|
||||
provider-database #1207
|
||||
|
||||
- account creation by scanning a qr code
|
||||
in the DCACCOUNT scheme (https://mailadm.readthedocs.io),
|
||||
new api is dc_set_config_from_qr() #1249
|
||||
|
||||
- if possible, dc_join_securejoin(), returns the new chat-id immediately
|
||||
and does the handshake in background #1225
|
||||
|
||||
- update imap and smtp dependencies #1115
|
||||
|
||||
- check for MOVE capability before using MOVE command #1263
|
||||
|
||||
- allow inline attachments from RFC 2183 #1280
|
||||
|
||||
- fix updating names from incoming mails #1298
|
||||
|
||||
- fix error messages shown on import #1234
|
||||
|
||||
- directly attempt to re-connect if the smtp connection is maybe stale #1296
|
||||
|
||||
- improve adding group members #1291
|
||||
|
||||
- improve rust-api #1261
|
||||
|
||||
- cleanup #1302 #1283 #1282 #1276 #1270-#1274 #1267 #1258-#1260
|
||||
#1257 #1239 #1231 #1224
|
||||
|
||||
- update spec #1286 #1291
|
||||
|
||||
|
||||
## 1.0.0-beta.24
|
||||
|
||||
- fix oauth2/gmail bug introduced in beta23 (not used in releases) #1219
|
||||
|
||||
- fix panic when receiving eg. cyrillic filenames #1216
|
||||
|
||||
- delete all consumed secure-join handshake messagess #1209 #1212
|
||||
|
||||
- rust-level cleanups #1218 #1217 #1210 #1205
|
||||
|
||||
- python-level cleanups #1204 #1202 #1201
|
||||
|
||||
|
||||
## 1.0.0-beta.23
|
||||
|
||||
- #1197 fix imap-deletion of messages
|
||||
|
||||
1384
Cargo.lock
generated
1384
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
30
Cargo.toml
30
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.0.0-beta.23"
|
||||
version = "1.28.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -12,7 +12,7 @@ lto = true
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
libc = "0.2.51"
|
||||
pgp = { version = "0.4.0", default-features = false }
|
||||
pgp = { version = "0.5.1", default-features = false }
|
||||
hex = "0.4.0"
|
||||
sha2 = "0.8.0"
|
||||
rand = "0.7.0"
|
||||
@@ -20,15 +20,12 @@ smallvec = "1.0.0"
|
||||
reqwest = { version = "0.10.0", features = ["blocking", "json"] }
|
||||
num-derive = "0.3.0"
|
||||
num-traits = "0.2.6"
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp" }
|
||||
async-smtp = "0.2"
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
|
||||
# XXX newer commits of async-imap lead to import-export tests hanging
|
||||
async-imap = { git = "https://github.com/async-email/async-imap", branch = "dcc-stable" }
|
||||
|
||||
async-native-tls = "0.1.1"
|
||||
async-std = { version = "1.0", features = ["unstable"] }
|
||||
async-imap = "0.2"
|
||||
async-native-tls = "0.3.1"
|
||||
async-std = { version = "1.4", features = ["unstable"] }
|
||||
base64 = "0.11"
|
||||
charset = "0.1"
|
||||
percent-encoding = "2.0"
|
||||
@@ -38,8 +35,6 @@ chrono = "0.4.6"
|
||||
failure = "0.1.5"
|
||||
failure_derive = "0.1.5"
|
||||
indexmap = "1.3.0"
|
||||
# TODO: make optional
|
||||
rustyline = "4.1.0"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.1.6"
|
||||
rusqlite = { version = "0.21", features = ["bundled"] }
|
||||
@@ -58,10 +53,14 @@ bitflags = "1.1.0"
|
||||
debug_stub_derive = "0.3.0"
|
||||
sanitize-filename = "0.2.1"
|
||||
stop-token = { version = "0.1.1", features = ["unstable"] }
|
||||
mailparse = "0.10.2"
|
||||
mailparse = "0.12.0"
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
native-tls = "0.2.3"
|
||||
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
|
||||
pretty_env_logger = "0.3.1"
|
||||
zip = "0.5"
|
||||
|
||||
rustyline = { version = "4.1.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
@@ -82,10 +81,11 @@ path = "examples/simple.rs"
|
||||
[[example]]
|
||||
name = "repl"
|
||||
path = "examples/repl/main.rs"
|
||||
required-features = ["rustyline"]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["nightly", "ringbuf"]
|
||||
vendored = ["native-tls/vendored", "reqwest/native-tls-vendored"]
|
||||
default = ["nightly"]
|
||||
vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"]
|
||||
nightly = ["pgp/nightly"]
|
||||
ringbuf = ["pgp/ringbuf"]
|
||||
|
||||
|
||||
@@ -108,7 +108,6 @@ $ cargo test -- --ignored
|
||||
|
||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||
- `nightly`: Enable nightly only performance and security related features.
|
||||
- `ringbuf`: Enable the use of [`slice_deque`](https://github.com/gnzlbg/slice_deque) in pgp.
|
||||
|
||||
[circle-shield]: https://img.shields.io/circleci/project/github/deltachat/deltachat-core-rust/master.svg?style=flat-square
|
||||
[circle]: https://circleci.com/gh/deltachat/deltachat-core-rust/
|
||||
|
||||
788
assets/exported-chat.css
Normal file
788
assets/exported-chat.css
Normal file
@@ -0,0 +1,788 @@
|
||||
/* TODO inlcude the referenced svgs as base64 data uris */
|
||||
|
||||
.header {
|
||||
background-color: #415e6b;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
height: 52px;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
display:flex;
|
||||
}
|
||||
|
||||
.header .avatar {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
border-radius: 100%;
|
||||
user-select: none;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.header .avatar.text-avatar {
|
||||
background-color: #505050;
|
||||
color: white;
|
||||
font-size: 26px;
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header .name {
|
||||
height: 52px;
|
||||
line-height: 52px;
|
||||
margin-left: 3px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.message.outgoing .author-avatar, .message.outgoing .author {
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
:root {--colorPrimary: #42A5F5;--colorDanger: #f96856;--colorNone: #a0a0a0;--ovalButtonBg: #415e6b;--ovalButtonBgHover: rgb(120, 156, 173);--ovalButtonText: #fff;--ovalButtonTextHover: rgb(0, 0, 0);--navBarBackground: #415e6b;--navBarText: #fff;--navBarSearchPlaceholder: rgb(186, 186, 186);--navBarGroupSubtitle: rgb(186, 186, 186);--chatViewBg: #e6dcd4;--chatViewBgImgPath: url(../images/background_light.svg);--composerBg: #fff;--composerText: #010101;--composerPlaceholderText: rgba(1, 1, 1, 0.5);--composerBtnColor: rgba(1, 1, 1, 0.9);--composerSendButton: #415e6b;--emojiSelectorSelectionColor: #2090ea;--chatListItemSelectedBg: #4c6e7d;--chatListItemSelectedBgHover: #5E889B;--chatListItemSelectedText: #fff;--chatListItemBgHover: rgb(228, 228, 228);--chatListBorderColor: #b9b9b9;--chatListBorder: 1px solid undefined;--messageText: #010101;--messageTextLink: #010101;--setupMessageText: #ed824e;--infoMessageBubbleBg: #0000008c;--infoMessageBubbleText: white;--messageIncommingBg: #fff;--messageIncommingDate: #010101;--messageOutgoingBg: #efffde;--messageOutgoingStatusColor: #4caf50;--messageButtons: #8b8e91;--messageButtonsHover: #070c14;--messageStatusIcon: #4caf50;--messageStatusIconSending: #62656a;--messagePadlockOutgoing: #4caf50;--messagePadlockIncomming: #a4a6a9;--messageMetadataDate: #62656a;--messageMetadataIncomming: rgba(#ffffff, 0.7);--messageAuthor: #ffffff;--messageAttachmentIconExtentionColor: #070c14;--messageAttachmentIconBg: transparent;--messageAttachmentFileInfo: #010101;--loginInputFocusColor: #42A5F5;--loginButtonText: #42A5F5;--deltaChatPrimaryFg: #010101;--deltaChatPrimaryFgLight: #62656a;--contextMenuBg: #fff;--contextMenuBorder: rgb(221, 221, 221);--contextMenuText: #62656a;--contextMenuSelected: #f5f5f5;--contextMenuSelectedBg: #a4a6a9;--bp3DialogHeaderBg: #fff;--bp3DialogHeaderIcon: #666666;--bp3DialogBgSecondary: #ececec;--bp3DialogBgPrimary: #fff;--bp3Heading: #010101;--bp3ButtonText: #010101;--bp3ButtonBg: #fff;--bp3ButtonGradientTop: rgba(255,255,255,0.8);--bp3ButtonGradientBottom: rgba(255,255,255,0);--bp3ButtonHoverBg: #ebf1f5;--bp3InputText: #010101;--bp3InputBg: #fff;--bp3InputPlaceholder: lightgray;--bp3MenuText: #010101;--bp3MenuBg: #fff;--bp3Switch: #7a8084;--bp3SwitchShadow: unset;--bp3SwitchChecked: #acd4e8;--bp3SwitchShadowChecked: unset;--bp3SwitchKnob: #f5f5f5;--bp3SwitchKnobShadow: 0px 2px 0 0px #d2cfcfad;--bp3SwitchKnobChecked: #42A5F5;--bp3SwitchKnobShadowChecked: 0px 1px 0 0px #c9d4d2d1;--bp3SpinnerTrack: #acd4e8;--bp3SpinnerHead: #42a5f5;--bp3SelectorTop: rgba(255, 255, 255, 0.8);--bp3SelectorBottom: rgba(255, 255, 255, 0.0);--outlineProperties: 1px solid transparent;--outlineColor: b9b9b9;--emojiMartText: #010101;--emojiMartSearchBorder: lightgrey;--emojiMartBg: #fff;--emojiMartOutsideRadius: 5px;--emojiMartCategoryIcons: rgb(99, 99, 99);--emojiMartInputBg: #f5f5f5;--emojiMartInputText: #010101;--emojiMartInputPlaceholder: rgb(74, 74, 74);--emojiMartSelect: rgb(198, 198, 198);--galleryBg: #fff;--avatarLabelColor: #ffffff;--brokenMediaText: #070c14;--brokenMediaBg: #ffffff;--unreadCountBg: #2090ea;--unreadCountLabel: #ffffff;--contactListItemBg: #62656a;--contactListInitalColor: #62656a;--contactEmailColor: #62656a;--errorColor: #f44336;--globalLinkColor: #2090ea;--globalBackground: #fff;--globalText: #010101;--mapOverlayBg: #fff;--videoPlayBtnIcon: #2090ea;--videoPlayBtnBg: #ffffff;--scrollbarThumb: #666666;--scrollbarThumbHover: #606060;}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html {
|
||||
height: 100%;
|
||||
--messageIncommingBg: rgb(232, 232, 232);
|
||||
}
|
||||
body {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-family: Roboto, "Apple Color Emoji", NotoEmoji, "Helvetica Neue", Arial,
|
||||
Helvetica, NotoMono, sans-serif !important;
|
||||
font-size: 14px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 0;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: white;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbarThumb);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbarThumbHover);
|
||||
}
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
span.module-contact-name {
|
||||
font-weight: 200;
|
||||
font-size: medium;
|
||||
}
|
||||
.module-contact-name__profile-name {
|
||||
font-style: italic;
|
||||
}
|
||||
.AvatarBubble {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
object-fit: cover;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 100%;
|
||||
background-color: #505050;
|
||||
color: var(--avatarLabelColor);
|
||||
font-size: 26px;
|
||||
line-height: 48px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
.AvatarBubble.large {
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
line-height: 64px;
|
||||
font-size: 39px;
|
||||
}
|
||||
.AvatarBubble--NoSearchResults {
|
||||
transform: rotate(45deg);
|
||||
line-height: 46px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.AvatarBubble--NoSearchResults::after {
|
||||
content: ":-(";
|
||||
}
|
||||
.AvatarImage {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
object-fit: cover;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
.AvatarImage.large {
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
}
|
||||
.attachment-overlay .attachment-view {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #313131;
|
||||
}
|
||||
.attachment-overlay .attachment-view img,
|
||||
.attachment-overlay .attachment-view video {
|
||||
width: 100vw;
|
||||
max-height: 100vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
.attachment-overlay .attachment-view video {
|
||||
width: 95vw;
|
||||
}
|
||||
.attachment-overlay .render-media-wrapper {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.attachment-overlay .btn-wrapper {
|
||||
float: right;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
}
|
||||
.attachment-overlay .download-btn {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
display: inline-block;
|
||||
-webkit-mask: url("../images/download.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: var(--messageButtons);
|
||||
}
|
||||
.attachment-overlay .download-btn:hover {
|
||||
background-color: var(--messageButtons);
|
||||
}
|
||||
.message-attachment-media {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin-left: -12px;
|
||||
margin-right: -12px;
|
||||
margin-top: -10px;
|
||||
margin-bottom: -10px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
background-color: var(--messageAttachmentIconBg);
|
||||
}
|
||||
.message-attachment-media > .attachment-content {
|
||||
object-fit: scale-down;
|
||||
object-position: center;
|
||||
min-height: 150px;
|
||||
max-height: 300px;
|
||||
max-width: 40vw;
|
||||
margin-bottom: -4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.message-attachment-media > .video-play-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-color: var(--videoPlayBtnBg);
|
||||
border-radius: 24px;
|
||||
}
|
||||
.message-attachment-media > .video-play-btn > .video-play-btn-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
-webkit-mask: url("../images/play.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: var(--videoPlayBtnIcon);
|
||||
}
|
||||
.message-attachment-media.content-below {
|
||||
margin-bottom: 7px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
.message-attachment-media.content-above {
|
||||
margin-top: 4px;
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
.message-attachment-broken-media {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.3px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
color: var(--brokenMediaBg);
|
||||
}
|
||||
.message-attachment-broken-media.incoming {
|
||||
color: var(--brokenMediaText);
|
||||
}
|
||||
.message-attachment-audio {
|
||||
margin-top: 2px;
|
||||
display: block;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.message-attachment-audio.content-below {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.message-attachment-audio.content-above {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.message-attachment-generic {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.message-attachment-generic.content-below {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.message-attachment-generic.content-above {
|
||||
padding-top: 4px;
|
||||
}
|
||||
.message-attachment-generic > .file-icon {
|
||||
background: url("../images/file-gradient.svg") no-repeat center;
|
||||
height: 44px;
|
||||
width: 56px;
|
||||
margin-left: -13px;
|
||||
margin-right: -14px;
|
||||
margin-bottom: -4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.message-attachment-generic > .file-icon > .file-extension {
|
||||
font-size: 10px;
|
||||
line-height: 13px;
|
||||
letter-spacing: 0.1px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
width: 25px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: clip;
|
||||
color: var(--messageAttachmentIconExtentionColor);
|
||||
font-family: monospace;
|
||||
}
|
||||
.message-attachment-generic > .text-part {
|
||||
flex-grow: 1;
|
||||
margin-left: 8px;
|
||||
max-width: calc(100% - 37px);
|
||||
}
|
||||
.message-attachment-generic > .text-part > .name {
|
||||
color: var(--messageAttachmentFileInfo);
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
font-weight: 300;
|
||||
margin-top: 2px;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.message-attachment-generic > .text-part > .size {
|
||||
color: var(--messageAttachmentFileInfo);
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.3px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.module-message-detail {
|
||||
margin-top: -20px;
|
||||
}
|
||||
.module-message-detail .bp3-callout {
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.module-message-detail p {
|
||||
white-space: pre-line;
|
||||
user-select: text;
|
||||
}
|
||||
.module-message-detail__message-container {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.module-message-detail__message-container:after {
|
||||
content: ".";
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
height: 0;
|
||||
clear: both;
|
||||
}
|
||||
.module-message-detail__label {
|
||||
font-weight: 300;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.module-message-detail__unix-timestamp {
|
||||
color: #eeefef;
|
||||
}
|
||||
.module-message-detail__delete-button-container {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.module-message-detail__delete-button {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
background-color: #f44336;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 10px -3px rgba(97, 97, 97, 0.7);
|
||||
border-radius: 5px;
|
||||
border: solid 1px #a4a6a9;
|
||||
cursor: pointer;
|
||||
margin: 1em auto;
|
||||
padding: 1em;
|
||||
}
|
||||
.module-message-detail .message-content * {
|
||||
background-color: lightgrey;
|
||||
width: 100%;
|
||||
resize: none;
|
||||
padding: 1rem;
|
||||
}
|
||||
.message-list-and-composer {
|
||||
width: 70%;
|
||||
float: right;
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
height: calc(100vh - 50px);
|
||||
margin-top: 50px;
|
||||
background-image: var(--chatViewBgImgPath);
|
||||
background-size: cover;
|
||||
background-color: var(--chatViewBg);
|
||||
}
|
||||
.message-list-and-composer__message-list #message-list {
|
||||
background: #dbdbdb;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
overflow: scroll;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
padding: 0 0.5em;
|
||||
top: 52px;
|
||||
}
|
||||
.message-list-and-composer__message-list
|
||||
#message-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.message-list-and-composer__message-list ul {
|
||||
list-style: none;
|
||||
min-width: 200px;
|
||||
}
|
||||
.message-list-and-composer__message-list ul li {
|
||||
margin-bottom: 10px;
|
||||
min-width: 200px;
|
||||
}
|
||||
.message-list-and-composer__message-list ul li::after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: " ";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
.message-list-and-composer__message-list ul li .info-message {
|
||||
max-width: 550px;
|
||||
font-size: 1rem;
|
||||
padding: 2rem;
|
||||
font-style: normal;
|
||||
white-space: pre-wrap;
|
||||
text-align: left;
|
||||
}
|
||||
.message {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
}
|
||||
.message:hover .message-buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
.message > .author-avatar {
|
||||
align-self: flex-end;
|
||||
bottom: 0px;
|
||||
position: static;
|
||||
margin-right: 8px;
|
||||
user-select: none;
|
||||
}
|
||||
.message > .author-avatar img {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
border-radius: 18px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.message > .author-avatar.default {
|
||||
text-align: center;
|
||||
}
|
||||
.message > .author-avatar.default > .label {
|
||||
user-select: none;
|
||||
color: var(--avatarLabelColor);
|
||||
top: -121px;
|
||||
left: -10px;
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 25px;
|
||||
line-height: 36px;
|
||||
}
|
||||
.message .message-buttons {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: -4px;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
z-index: 10;
|
||||
user-select: text;
|
||||
}
|
||||
.message .message-buttons .msg-button {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.message .message-buttons .msg-button:hover {
|
||||
background-color: var(--messageButtons);
|
||||
}
|
||||
.message .message-buttons .msg-button.download {
|
||||
-webkit-mask: url("../images/download.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: var(--messageButtons);
|
||||
}
|
||||
.message .message-buttons .msg-button.reply {
|
||||
display: none;
|
||||
-webkit-mask: url("../images/reply.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: var(--messageButtons);
|
||||
user-select: none;
|
||||
}
|
||||
.message .message-buttons .msg-button.menu {
|
||||
-webkit-mask: url("../images/ellipsis.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: var(--messageButtons);
|
||||
transform: rotate(90deg);
|
||||
-webkit-mask-position-y: 4px;
|
||||
user-select: none;
|
||||
}
|
||||
.message .msg-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-radius: 16px;
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.message .msg-container > .author {
|
||||
display: inline-block;
|
||||
max-width: 40vw;
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
line-height: 18px;
|
||||
height: 18px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.message .msg-container .msg-body.msg-body--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.message .msg-container .msg-body > .text {
|
||||
color: var(--messageText);
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
text-align: start;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.message .msg-container .msg-body > .text a {
|
||||
text-decoration: underline;
|
||||
color: var(--messageTextLink);
|
||||
}
|
||||
.message .msg-container .msg-body > .text .double-line-break {
|
||||
height: 28px;
|
||||
}
|
||||
.message .msg-container .msg-body > .text .line-break {
|
||||
height: 14px;
|
||||
}
|
||||
.message .msg-container .msg-body > .text .line-break:last-child {
|
||||
height: 0px;
|
||||
}
|
||||
.message .metadata {
|
||||
margin-top: 10px;
|
||||
margin-bottom: -7px;
|
||||
float: right;
|
||||
}
|
||||
.message .module-message__img-attachment {
|
||||
object-fit: cover;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
min-height: unset;
|
||||
}
|
||||
.message.incoming {
|
||||
margin-left: 0;
|
||||
margin-right: 32px;
|
||||
}
|
||||
.message.incoming .metadata:not(.with-image-no-caption) > .padlock-icon {
|
||||
-webkit-mask: url("../images/padlock.svg") no-repeat center;
|
||||
-webkit-mask-size: 125%;
|
||||
background-color: var(--messagePadlockIncomming);
|
||||
}
|
||||
.message.incoming .metadata:not(.with-image-no-caption) > .location-icon {
|
||||
-webkit-mask: url("../images/map-marker.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: var(--messagePadlockIncomming);
|
||||
}
|
||||
.message.incoming .metadata:not(.with-image-no-caption) > .date {
|
||||
color: var(--messageMetadataIncomming);
|
||||
}
|
||||
.message.incoming .msg-container {
|
||||
background-color: var(--messageIncommingBg);
|
||||
}
|
||||
.message.incoming .msg-container,
|
||||
.message.incoming .msg-container .message-attachment-media {
|
||||
border-bottom-left-radius: 1px;
|
||||
}
|
||||
.message.outgoing {
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
margin-left: 32px;
|
||||
}
|
||||
.message.outgoing .metadata > .date {
|
||||
color: var(--messageOutgoingStatusColor);
|
||||
}
|
||||
.message.outgoing .metadata > .padlock-icon {
|
||||
-webkit-mask: url("../images/padlock.svg") no-repeat center;
|
||||
-webkit-mask-size: 125%;
|
||||
background-color: var(--messagePadlockOutgoing);
|
||||
}
|
||||
.message.outgoing .metadata > .location-icon {
|
||||
-webkit-mask: url("../images/map-marker.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: var(--messagePadlockOutgoing);
|
||||
}
|
||||
.message.outgoing .metadata > .status-icon.read,
|
||||
.message.outgoing .metadata > .status-icon.delivered {
|
||||
background-color: var(--messageOutgoingStatusColor);
|
||||
}
|
||||
.message.outgoing .msg-container {
|
||||
background-color: var(--messageOutgoingBg);
|
||||
}
|
||||
.message.outgoing .msg-container,
|
||||
.message.outgoing .msg-container .message-attachment-media {
|
||||
border-bottom-right-radius: 1px;
|
||||
}
|
||||
.message.type-sticker .msg-container {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.message.type-sticker .message-attachment-media {
|
||||
background-color: transparent;
|
||||
}
|
||||
.message.type-sticker .message-attachment-media > .attachment-content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.message.type-sticker .metadata {
|
||||
float: right;
|
||||
padding: 4px 10px 1px 10px;
|
||||
margin-bottom: -7px;
|
||||
background-color: #01010159;
|
||||
border-radius: 4px;
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
}
|
||||
.message.type-sticker .metadata > .date {
|
||||
font-size: 11px;
|
||||
color: white;
|
||||
}
|
||||
.message.type-sticker .metadata > .padlock-icon {
|
||||
-webkit-mask: url("../images/padlock.svg") no-repeat center;
|
||||
-webkit-mask-size: 125%;
|
||||
background-color: #fff;
|
||||
}
|
||||
.message.type-sticker .metadata > .location-icon {
|
||||
-webkit-mask: url("../images/map-marker.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
.message.type-sticker .status-icon.read,
|
||||
.message.type-sticker .status-icon.delivered {
|
||||
background-color: white;
|
||||
}
|
||||
.message.type-sticker:hover .msg-button.menu {
|
||||
background-color: white;
|
||||
}
|
||||
.message.type-sticker:hover .react-contextmenu-wrapper {
|
||||
background-color: #2525258f;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.message.error.incoming .text {
|
||||
font-style: italic;
|
||||
}
|
||||
.message.forwarded .forwarded-indicator {
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 3px;
|
||||
opacity: 0.86;
|
||||
}
|
||||
.message.forwarded .message-attachment-media {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
.setupMessage .message .text {
|
||||
color: var(--setupMessageText);
|
||||
}
|
||||
.hide-on-small {
|
||||
display: initial;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.hide-on-small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (min-width: 800px) and (max-width: 925px) {
|
||||
.message {
|
||||
max-width: 374px;
|
||||
}
|
||||
.message.incoming {
|
||||
margin-right: auto;
|
||||
}
|
||||
.message.outgoing {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
@media (min-width: 926px) {
|
||||
.message {
|
||||
max-width: 66%;
|
||||
}
|
||||
.message.incoming {
|
||||
margin-right: auto;
|
||||
}
|
||||
.message.outgoing {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
.metadata {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
.metadata.with-image-no-caption {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
float: right;
|
||||
padding: 4px 10px 1px 10px;
|
||||
margin: 0;
|
||||
background-color: #0000008f;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.metadata.with-image-no-caption > .date {
|
||||
color: white;
|
||||
}
|
||||
.metadata.with-image-no-caption > .padlock-icon {
|
||||
-webkit-mask: url("../images/padlock.svg") no-repeat center;
|
||||
-webkit-mask-size: 125%;
|
||||
background-color: #fff;
|
||||
}
|
||||
.metadata.with-image-no-caption .status-icon.sending {
|
||||
background-color: white;
|
||||
}
|
||||
.metadata > .status-icon {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.metadata > .username {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.metadata > .date {
|
||||
font-size: 11.5px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.3px;
|
||||
color: var(--messageMetadataDate);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.metadata > .spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.metadata > .padlock-icon,
|
||||
.metadata > .location-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.metadata > .location-icon {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@keyframes __status-icon--spinning {
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.status-icon {
|
||||
width: 18px;
|
||||
height: 12px;
|
||||
display: inline-block;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.status-icon.sending {
|
||||
-webkit-mask: url("../images/sending.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: var(--messageStatusIconSending);
|
||||
animation: __status-icon--spinning 4s linear infinite;
|
||||
width: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.status-icon.delivered {
|
||||
-webkit-mask: url("../images/sent.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: var(--messageStatusIcon);
|
||||
}
|
||||
.status-icon.read {
|
||||
-webkit-mask: url("../images/read.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: var(--messageStatusIcon);
|
||||
}
|
||||
.status-icon.error {
|
||||
-webkit-mask: url("../images/error.svg") no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: var(--errorColor);
|
||||
width: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
@@ -46,6 +46,7 @@ if [ -n "$TESTS" ]; then
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
|
||||
unset DCC_PY_LIVECONFIG
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
|
||||
tox --workdir "$TOXWORKDIR" -e auditwheels
|
||||
popd
|
||||
|
||||
@@ -32,11 +32,11 @@ ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
|
||||
set +x -e
|
||||
cd $BUILDDIR
|
||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
set -x
|
||||
|
||||
# run everything else inside docker
|
||||
docker run -e DCC_PY_LIVECONFIG \
|
||||
docker run -e DCC_NEW_TMP_EMAIL -e DCC_PY_LIVECONFIG \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps ci_scripts/run_all.sh
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ ssh $SSHTARGET <<_HERE
|
||||
export CARGO_TARGET_DIR=\`pwd\`/../target
|
||||
export TARGET=release
|
||||
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
||||
|
||||
#we rely on tox/virtualenv being available in the host
|
||||
#rm -rf virtualenv venv
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
set -e -x
|
||||
|
||||
# for core-building and python install step
|
||||
export DCC_RS_TARGET=release
|
||||
export DCC_RS_TARGET=debug
|
||||
export DCC_RS_DEV=`pwd`
|
||||
|
||||
cd python
|
||||
|
||||
@@ -37,7 +37,8 @@ mkdir -p $TOXWORKDIR
|
||||
# XXX we may switch on some live-tests on for better ensurances
|
||||
# Note that the independent remote_tests_python step does all kinds of
|
||||
# live-testing already.
|
||||
unset DCC_PY_LIVECONFIG
|
||||
unset DCC_PY_LIVECONFIG
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
|
||||
popd
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.0.0-beta.23"
|
||||
version = "1.28.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -16,7 +16,6 @@ crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
deltachat = { path = "../", default-features = false }
|
||||
deltachat-provider-database = "0.2.1"
|
||||
libc = "0.2"
|
||||
human-panic = "1.0.1"
|
||||
num-traits = "0.2.6"
|
||||
@@ -24,7 +23,6 @@ failure = "0.1.6"
|
||||
serde_json = "1.0"
|
||||
|
||||
[features]
|
||||
default = ["vendored", "nightly", "ringbuf"]
|
||||
default = ["vendored", "nightly"]
|
||||
vendored = ["deltachat/vendored"]
|
||||
nightly = ["deltachat/nightly"]
|
||||
ringbuf = ["deltachat/ringbuf"]
|
||||
|
||||
@@ -364,9 +364,23 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* also show all mails of confirmed contacts,
|
||||
* DC_SHOW_EMAILS_ALL (2)=
|
||||
* also show mails of unconfirmed contacts in the deaddrop.
|
||||
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
|
||||
* generate recommended key type (default),
|
||||
* DC_KEY_GEN_RSA2048 (1)=
|
||||
* generate RSA 2048 keypair
|
||||
* DC_KEY_GEN_ED25519 (2)=
|
||||
* generate Ed25519 keypair
|
||||
* - `save_mime_headers` = 1=save mime headers
|
||||
* and make dc_get_mime_headers() work for subsequent calls,
|
||||
* 0=do not save mime headers (default)
|
||||
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
|
||||
* >=1=seconds, after which messages are deleted automatically from the device.
|
||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
|
||||
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
|
||||
* >=1=seconds, after which messages are deleted automatically from the server.
|
||||
* "Saved messages" are deleted from the server as well as
|
||||
* emails matching the `show_emails` settings above, the UI should clearly point that out.
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -403,6 +417,7 @@ int dc_set_config (dc_context_t* context, const char*
|
||||
*/
|
||||
char* dc_get_config (dc_context_t* context, const char* key);
|
||||
|
||||
|
||||
/**
|
||||
* Set stock string translation.
|
||||
*
|
||||
@@ -417,6 +432,22 @@ char* dc_get_config (dc_context_t* context, const char*
|
||||
int dc_set_stock_translation(dc_context_t* context, uint32_t stock_id, const char* stock_msg);
|
||||
|
||||
|
||||
/**
|
||||
* Set configuration values from a QR code containing an account.
|
||||
* Before this function is called, dc_check_qr() should confirm the type of the
|
||||
* QR code is DC_QR_ACCOUNT.
|
||||
*
|
||||
* Internally, the function will call dc_set_config()
|
||||
* at least with the keys `addr` and `mail_pw`.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object
|
||||
* @param qr scanned QR code
|
||||
* @return int (==0 on error, 1 on success)
|
||||
*/
|
||||
int dc_set_config_from_qr (dc_context_t* context, const char* qr);
|
||||
|
||||
|
||||
/**
|
||||
* Get information about the context.
|
||||
*
|
||||
@@ -852,11 +883,33 @@ void dc_interrupt_smtp_idle (dc_context_t* context);
|
||||
void dc_maybe_network (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Save a keypair as the default keys for the user.
|
||||
*
|
||||
* This API is only for testing purposes and should not be used as part of a
|
||||
* normal application, use the import-export APIs instead.
|
||||
*
|
||||
* This saves a public/private keypair as the default keypair in the context.
|
||||
* It allows avoiding having to generate a secret key for unittests which need
|
||||
* one.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param addr The email address of the user. This must match the
|
||||
* configured_addr setting of the context as well as the UID of the key.
|
||||
* @param public_data The public key as base64.
|
||||
* @param secret_data The secret key as base64.
|
||||
* @return 1 on success, 0 on failure.
|
||||
*/
|
||||
int dc_preconfigure_keypair (dc_context_t* context, const char *addr, const char *public_data, const char *secret_data);
|
||||
|
||||
|
||||
// handle chatlists
|
||||
|
||||
#define DC_GCL_ARCHIVED_ONLY 0x01
|
||||
#define DC_GCL_NO_SPECIALS 0x02
|
||||
#define DC_GCL_ADD_ALLDONE_HINT 0x04
|
||||
#define DC_GCL_FOR_FORWARDING 0x08
|
||||
|
||||
|
||||
/**
|
||||
@@ -880,7 +933,7 @@ void dc_maybe_network (dc_context_t* context);
|
||||
* or "Not now".
|
||||
* The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
|
||||
* - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
|
||||
* archived _any_ chat using dc_archive_chat(). The UI should show a link as
|
||||
* archived _any_ chat using dc_set_chat_visibility(). The UI should show a link as
|
||||
* "Show archived chats", if the user clicks this item, the UI should show a
|
||||
* list of all archived chats that can be created by this function hen using
|
||||
* the DC_GCL_ARCHIVED_ONLY flag.
|
||||
@@ -895,6 +948,8 @@ void dc_maybe_network (dc_context_t* context);
|
||||
* if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
|
||||
* the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
|
||||
* chats
|
||||
* - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist,
|
||||
* typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
* - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
||||
* to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||
* not needed when DC_GCL_ARCHIVED_ONLY is already set)
|
||||
@@ -1248,6 +1303,21 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
|
||||
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Estimate the number of messages that will be deleted
|
||||
* by the dc_set_config()-options `delete_device_after` or `delete_server_after`.
|
||||
* This is typically used to show the estimated impact to the user before actually enabling ephemeral messages.
|
||||
*
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
|
||||
* @param seconds Count messages older than the given number of seconds.
|
||||
* @return Number of messages that are older than the given number of seconds.
|
||||
* This includes emails downloaded due to the `show_emails` option.
|
||||
* Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
|
||||
*/
|
||||
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
|
||||
|
||||
/**
|
||||
* Returns the message IDs of all _fresh_ messages of any chat.
|
||||
* Typically used for implementing notification summaries.
|
||||
@@ -1334,25 +1404,18 @@ uint32_t dc_get_next_media (dc_context_t* context, uint32_t ms
|
||||
|
||||
|
||||
/**
|
||||
* Archive or unarchive a chat.
|
||||
* Set chat visibility to pinned, archived or normal.
|
||||
*
|
||||
* Archived chats are not included in the default chatlist returned
|
||||
* by dc_get_chatlist(). Instead, if there are _any_ archived chats,
|
||||
* the pseudo-chat with the chat_id DC_CHAT_ID_ARCHIVED_LINK will be added the the
|
||||
* end of the chatlist.
|
||||
*
|
||||
* - To get a list of archived chats, use dc_get_chatlist() with the flag DC_GCL_ARCHIVED_ONLY.
|
||||
* - To find out the archived state of a given chat, use dc_chat_get_archived()
|
||||
* - Messages in archived chats are marked as being noticed, so they do not count as "fresh"
|
||||
* - Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED
|
||||
* Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED
|
||||
* See @ref DC_CHAT_VISIBILITY for detailed information about the visibilities.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The ID of the chat to archive or unarchive.
|
||||
* @param archive 1=archive chat, 0=unarchive chat, all other values are reserved for future use
|
||||
* @param chat_id The ID of the chat to change the visibility for.
|
||||
* @param visibility one of @ref DC_CHAT_VISIBILITY
|
||||
* @return None.
|
||||
*/
|
||||
void dc_archive_chat (dc_context_t* context, uint32_t chat_id, int archive);
|
||||
void dc_set_chat_visibility (dc_context_t* context, uint32_t chat_id, int visibility);
|
||||
|
||||
|
||||
/**
|
||||
@@ -1556,6 +1619,22 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
|
||||
int dc_set_chat_profile_image (dc_context_t* context, uint32_t chat_id, const char* image);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Set mute duration of a chat.
|
||||
*
|
||||
* This value can be checked by the ui upon receiving a new message to decide whether it should trigger an notification.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED.
|
||||
*
|
||||
* @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().
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_set_chat_mute_duration (dc_context_t* context, uint32_t chat_id, int64_t duration);
|
||||
|
||||
// handle messages
|
||||
|
||||
/**
|
||||
@@ -1602,8 +1681,9 @@ char* dc_get_mime_headers (dc_context_t* context, uint32_t ms
|
||||
*/
|
||||
void dc_delete_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||
|
||||
/**
|
||||
/*
|
||||
* Empty IMAP server folder: delete all messages.
|
||||
* 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()
|
||||
@@ -1756,7 +1836,7 @@ uint32_t dc_create_contact (dc_context_t* context, const char*
|
||||
* Trying to add email-addresses that are already in the contact list,
|
||||
* results in updating the name unless the name was changed manually by the user.
|
||||
* If any email-address or any name is really updated,
|
||||
* the event DC_EVENT_CONTACTS_CHANGED is sent.
|
||||
* the event #DC_EVENT_CONTACTS_CHANGED is sent.
|
||||
*
|
||||
* To add a single contact entered by the user, you should prefer dc_create_contact(),
|
||||
* however, for adding a bunch of addresses, this function is _much_ faster.
|
||||
@@ -2074,6 +2154,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_FPR_OK 210 // id=contact
|
||||
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
#define DC_QR_URL 332 // text1=URL
|
||||
@@ -2091,12 +2172,12 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID
|
||||
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID
|
||||
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
|
||||
* - DC_QR_ACCOUNT allows creation of an account, dc_lot_t::text1=domain
|
||||
* - DC_QR_ADDR with dc_lot_t::id=Contact ID
|
||||
* - DC_QR_TEXT with dc_lot_t::text1=Text
|
||||
* - DC_QR_URL with dc_lot_t::text1=URL
|
||||
* - DC_QR_ERROR with dc_lot_t::text1=Error string
|
||||
*
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param qr The text of the scanned QR code.
|
||||
@@ -2107,20 +2188,22 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
|
||||
|
||||
/**
|
||||
* Get QR code text that will offer an secure-join verification.
|
||||
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
|
||||
* The QR code is compatible to the OPENPGP4FPR format
|
||||
* so that a basic fingerprint comparison also works eg. with OpenKeychain.
|
||||
*
|
||||
* The scanning device will pass the scanned content to dc_check_qr() then;
|
||||
* if this function returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
|
||||
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
|
||||
* an out-of-band-verification can be joined using dc_join_securejoin()
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id If set to a group-chat-id,
|
||||
* the group-join-protocol is offered in the QR code;
|
||||
* the Verified-Group-Invite protocol is offered in the QR code;
|
||||
* works for verified groups as well as for normal groups.
|
||||
* If set to 0, the setup-Verified-contact-protocol is offered in the QR code.
|
||||
* If set to 0, the Setup-Contact protocol is offered in the QR code.
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
* @return Text that should go to the QR code,
|
||||
* On errors, an empty QR code is returned, NULL is never returned.
|
||||
* The returned string must be released using dc_str_unref() after usage.
|
||||
@@ -2129,13 +2212,29 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
|
||||
|
||||
|
||||
/**
|
||||
* Join an out-of-band-verification initiated on another device with dc_get_securejoin_qr().
|
||||
* Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
* started on another device with dc_get_securejoin_qr().
|
||||
* This function is typically called when dc_check_qr() returns
|
||||
* lot.state=DC_QR_ASK_VERIFYCONTACT or lot.state=DC_QR_ASK_VERIFYGROUP.
|
||||
*
|
||||
* This function takes some time and sends and receives several messages.
|
||||
* You should call it in a separate thread; if you want to abort it, you should
|
||||
* call dc_stop_ongoing_process().
|
||||
* Depending on the given QR code,
|
||||
* this function may takes some time and sends and receives several messages.
|
||||
* Therefore, you should call it always in a separate thread;
|
||||
* if you want to abort it, you should call dc_stop_ongoing_process().
|
||||
*
|
||||
* - If the given QR code starts the Setup-Contact protocol,
|
||||
* the function typically returns immediately
|
||||
* and the handshake runs in background.
|
||||
* Subsequent calls of dc_join_securejoin() will abort unfinished tasks.
|
||||
* The returned chat is the one-to-one opportunistic chat.
|
||||
* When the protocol has finished, an info-message is added to that chat.
|
||||
* - If the given QR code starts the Verified-Group-Invite protocol,
|
||||
* the function waits until the protocol has finished.
|
||||
* This is because the verified group is not opportunistic
|
||||
* and can be created only when the contacts have verified each other.
|
||||
*
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object
|
||||
@@ -2143,6 +2242,9 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
|
||||
* to dc_check_qr().
|
||||
* @return Chat-id of the joined chat, the UI may redirect to the this chat.
|
||||
* If the out-of-band verification failed or was aborted, 0 is returned.
|
||||
* A returned chat-id does not guarantee that the chat or the belonging contact is verified.
|
||||
* If needed, this be checked with dc_chat_is_verified() and dc_contact_is_verified(),
|
||||
* however, in practise, the UI will just listen to #DC_EVENT_CONTACTS_CHANGED unconditionally.
|
||||
*/
|
||||
uint32_t dc_join_securejoin (dc_context_t* context, const char* qr);
|
||||
|
||||
@@ -2770,21 +2872,14 @@ uint32_t dc_chat_get_color (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Get archived state.
|
||||
*
|
||||
* - 0 = normal chat, not archived, not sticky.
|
||||
* - 1 = chat archived
|
||||
* - 2 = chat sticky (reserved for future use, if you do not support this value, just treat the chat as a normal one)
|
||||
*
|
||||
* To archive or unarchive chats, use dc_archive_chat().
|
||||
* If chats are archived, this should be shown in the UI by a little icon or text,
|
||||
* eg. the search will also return archived chats.
|
||||
* Get visibility of chat.
|
||||
* See @ref DC_CHAT_VISIBILITY for detailed information about the visibilities.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return Archived state.
|
||||
* @return One of @ref DC_CHAT_VISIBILITY
|
||||
*/
|
||||
int dc_chat_get_archived (const dc_chat_t* chat);
|
||||
int dc_chat_get_visibility (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
@@ -2877,6 +2972,26 @@ int dc_chat_is_verified (const dc_chat_t* chat);
|
||||
int dc_chat_is_sending_locations (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check whether the chat is currently muted
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=muted, 0=not muted
|
||||
*/
|
||||
int dc_chat_is_muted (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Get the exact state of the mute of a chat
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 0=not muted, -1=forever muted, (x>0)=remaining seconds until the mute is lifted
|
||||
*/
|
||||
int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_msg_t
|
||||
*
|
||||
@@ -3665,30 +3780,19 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Create a provider struct for the given domain.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param domain The domain to get provider info for.
|
||||
* @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
|
||||
* returned.
|
||||
*/
|
||||
dc_provider_t* dc_provider_new_from_domain (const char* domain);
|
||||
|
||||
|
||||
/**
|
||||
* Create a provider struct for the given email address.
|
||||
*
|
||||
* 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 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
|
||||
* returned.
|
||||
*/
|
||||
dc_provider_t* dc_provider_new_from_email (const char* email);
|
||||
dc_provider_t* dc_provider_new_from_email (const dc_context_t* context, const char* email);
|
||||
|
||||
|
||||
/**
|
||||
@@ -3698,53 +3802,35 @@ dc_provider_t* dc_provider_new_from_email (const char* email);
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
* @return A string which must be released using dc_str_unref().
|
||||
* @return String with a fully-qualified URL,
|
||||
* if there is no such URL, an empty string is returned, NULL is never returned.
|
||||
* The returned value must be released using dc_str_unref().
|
||||
*/
|
||||
char* dc_provider_get_overview_page (const dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* The provider's name.
|
||||
* Get hints to be shown to the user on the login screen.
|
||||
* Depending on the @ref DC_PROVIDER_STATUS returned by dc_provider_get_status(),
|
||||
* the ui may want to highlight the hint.
|
||||
*
|
||||
* The name of the provider, e.g. "POSTEO".
|
||||
* Moreover, the ui should display a "More information" link
|
||||
* that forwards to the url returned by dc_provider_get_overview_page().
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
* @return A string which must be released using dc_str_unref().
|
||||
* @return A string with the hint to show to the user, may contain multiple lines,
|
||||
* if there is no such hint, an empty string is returned, NULL is never returned.
|
||||
* The returned value must be released using dc_str_unref().
|
||||
*/
|
||||
char* dc_provider_get_name (const dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* The markdown content of the providers page.
|
||||
*
|
||||
* This contains the preparation steps or additional information if the status
|
||||
* is @ref DC_PROVIDER_STATUS_BROKEN.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
* @return A string which must be released using dc_str_unref().
|
||||
*/
|
||||
char* dc_provider_get_markdown (const dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* Date of when the state was last checked/updated.
|
||||
*
|
||||
* This is returned as a string.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
* @return A string which must be released using dc_str_unref().
|
||||
*/
|
||||
char* dc_provider_get_status_date (const dc_provider_t* provider);
|
||||
char* dc_provider_get_before_login_hint (const dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
* Whether DC works with this provider.
|
||||
*
|
||||
* Can be one of @ref DC_PROVIDER_STATUS_OK, @ref
|
||||
* DC_PROVIDER_STATUS_PREPARATION and @ref DC_PROVIDER_STATUS_BROKEN.
|
||||
* Can be one of #DC_PROVIDER_STATUS_OK,
|
||||
* #DC_PROVIDER_STATUS_PREPARATION or #DC_PROVIDER_STATUS_BROKEN.
|
||||
*
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
@@ -3759,7 +3845,7 @@ int dc_provider_get_status (const dc_provider_t* prov
|
||||
* @memberof dc_provider_t
|
||||
* @param provider The dc_provider_t struct.
|
||||
*/
|
||||
void dc_provider_unref (const dc_provider_t* provider);
|
||||
void dc_provider_unref (dc_provider_t* provider);
|
||||
|
||||
|
||||
/**
|
||||
@@ -4065,28 +4151,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_EMPTY DC_EMPTY
|
||||
*
|
||||
* These constants configure emptying imap folders with dc_empty_server()
|
||||
*
|
||||
* @addtogroup DC_EMPTY
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Clear all mvbox messages.
|
||||
*/
|
||||
#define DC_EMPTY_MVBOX 0x01
|
||||
|
||||
/**
|
||||
* Clear all INBOX messages.
|
||||
*/
|
||||
#define DC_EMPTY_INBOX 0x02
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
#define DC_EMPTY_MVBOX 0x01 // Deprecated, flag for dc_empty_server(): Clear all mvbox messages
|
||||
#define DC_EMPTY_INBOX 0x02 // Deprecated, flag for dc_empty_server(): Clear all INBOX messages
|
||||
|
||||
|
||||
/**
|
||||
@@ -4347,7 +4413,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
|
||||
|
||||
/**
|
||||
* Contact(s) created, renamed, blocked or deleted.
|
||||
* Contact(s) created, renamed, verified, blocked or deleted.
|
||||
*
|
||||
* @param data1 (int) If not 0, this is the contact_id of an added contact that should be selected.
|
||||
* @param data2 0
|
||||
@@ -4465,6 +4531,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore
|
||||
char* dc_get_version_str (void); // deprecated
|
||||
void dc_array_add_id (dc_array_t*, uint32_t); // deprecated
|
||||
#define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore
|
||||
#define dc_chat_get_archived(a) (dc_chat_get_visibility((a))==1? 1 : 0) // not used anymore
|
||||
|
||||
|
||||
/*
|
||||
@@ -4474,6 +4542,13 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
|
||||
#define DC_SHOW_EMAILS_ACCEPTED_CONTACTS 1
|
||||
#define DC_SHOW_EMAILS_ALL 2
|
||||
|
||||
/*
|
||||
* Values for dc_get|set_config("key_gen_type")
|
||||
*/
|
||||
#define DC_KEY_GEN_DEFAULT 0
|
||||
#define DC_KEY_GEN_RSA2048 1
|
||||
#define DC_KEY_GEN_ED25519 2
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
|
||||
@@ -4485,23 +4560,43 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provider status returned by dc_provider_get_status().
|
||||
* Prover works out-of-the-box.
|
||||
* This provider status is returned for provider where the login
|
||||
* works by just entering the name or the email-address.
|
||||
*
|
||||
* Works right out of the box without any preperation steps needed
|
||||
* - There is no need for the user to do any special things
|
||||
* (enable IMAP or so) in the provider's webinterface or at other places.
|
||||
* - There is no need for the user to enter advanced settings;
|
||||
* server, port etc. are known by the core.
|
||||
*
|
||||
* The status is returned by dc_provider_get_status().
|
||||
*/
|
||||
#define DC_PROVIDER_STATUS_OK 1
|
||||
|
||||
/**
|
||||
* Provider status returned by dc_provider_get_status().
|
||||
* Provider works, but there are preparations needed.
|
||||
*
|
||||
* Works, but preparation steps are needed
|
||||
* - The user has to do some special things as "Enable IMAP in the Webinterface",
|
||||
* what exactly, is described in the string returnd by dc_provider_get_before_login_hints()
|
||||
* and, typically more detailed, in the page linked by dc_provider_get_overview_page().
|
||||
* - There is no need for the user to enter advanced settings;
|
||||
* server, port etc. should be known by the core.
|
||||
*
|
||||
* The status is returned by dc_provider_get_status().
|
||||
*/
|
||||
#define DC_PROVIDER_STATUS_PREPARATION 2
|
||||
|
||||
/**
|
||||
* Provider status returned by dc_provider_get_status().
|
||||
* Provider is not working.
|
||||
* This provider status is returned for providers
|
||||
* that are known to not work with Delta Chat.
|
||||
* The ui should block logging in with this provider.
|
||||
*
|
||||
* Doesn't work (too unstable to use falls also in this category)
|
||||
* More information about that is typically provided
|
||||
* in the string returned by dc_provider_get_before_login_hints()
|
||||
* and in the page linked by dc_provider_get_overview_page().
|
||||
*
|
||||
* The status is returned by dc_provider_get_status().
|
||||
*/
|
||||
#define DC_PROVIDER_STATUS_BROKEN 3
|
||||
|
||||
@@ -4510,6 +4605,48 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_CHAT_VISIBILITY DC_CHAT_VISIBILITY
|
||||
*
|
||||
* These constants describe the visibility of a chat.
|
||||
* The chat visibiliry can be get using dc_chat_get_visibility()
|
||||
* and set using dc_set_chat_visibility().
|
||||
*
|
||||
* @addtogroup DC_CHAT_VISIBILITY
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Chats with normal visibility are not archived and are shown below all pinned chats.
|
||||
* Archived chats, that receive new messages automatically become normal chats.
|
||||
*/
|
||||
#define DC_CHAT_VISIBILITY_NORMAL 0
|
||||
|
||||
/**
|
||||
* Archived chats are not included in the default chatlist returned by dc_get_chatlist().
|
||||
* Instead, if there are _any_ archived chats, the pseudo-chat
|
||||
* with the chat_id DC_CHAT_ID_ARCHIVED_LINK will be added the the end of the chatlist.
|
||||
*
|
||||
* The UI typically shows a little icon or chats beside archived chats in the chatlist,
|
||||
* this is needed as eg. the search will also return archived chats.
|
||||
*
|
||||
* If archived chats receive new messages, they become normal chats again.
|
||||
*
|
||||
* To get a list of archived chats, use dc_get_chatlist() with the flag DC_GCL_ARCHIVED_ONLY.
|
||||
*/
|
||||
#define DC_CHAT_VISIBILITY_ARCHIVED 1
|
||||
|
||||
/**
|
||||
* Pinned chats are included in the default chatlist. moreover,
|
||||
* they are always the first items, whether they have fresh messages or not.
|
||||
*/
|
||||
#define DC_CHAT_VISIBILITY_PINNED 2
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* TODO: Strings need some doumentation about used placeholders.
|
||||
*
|
||||
|
||||
@@ -20,14 +20,16 @@ use std::fmt::Write;
|
||||
use std::ptr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::RwLock;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use libc::uintptr_t;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::stock::StockMessage;
|
||||
use deltachat::*;
|
||||
@@ -83,15 +85,12 @@ pub type dc_callback_t =
|
||||
pub type dc_context_t = ContextWrapper;
|
||||
|
||||
impl ContextWrapper {
|
||||
/// Log an error on the FFI context.
|
||||
/// Log a warning on the FFI context.
|
||||
///
|
||||
/// As soon as a [ContextWrapper] exist it can be used to log an
|
||||
/// error using the callback, even before [dc_context_open] is
|
||||
/// called and an actual [Context] exists.
|
||||
///
|
||||
/// This function makes it easy to log an error.
|
||||
unsafe fn error(&self, msg: &str) {
|
||||
self.translate_cb(Event::Error(msg.to_string()));
|
||||
/// Like [error] but logs as a warning which only goes to the
|
||||
/// logfile rather than being shown directly to the user.
|
||||
unsafe fn warning(&self, msg: &str) {
|
||||
self.translate_cb(Event::Warning(msg.to_string()));
|
||||
}
|
||||
|
||||
/// Unlock the context and execute a closure with it.
|
||||
@@ -109,17 +108,29 @@ impl ContextWrapper {
|
||||
/// the appropriate return value for an error return since this
|
||||
/// differs for various functions on the FFI API: sometimes 0,
|
||||
/// NULL, an empty string etc.
|
||||
fn with_inner<T, F>(&self, ctxfn: F) -> Result<T, ()>
|
||||
unsafe fn with_inner<T, F>(&self, ctxfn: F) -> Result<T, ()>
|
||||
where
|
||||
F: FnOnce(&Context) -> T,
|
||||
{
|
||||
self.try_inner(|ctx| Ok(ctxfn(ctx))).map_err(|err| {
|
||||
self.warning(&err.to_string());
|
||||
})
|
||||
}
|
||||
|
||||
/// Unlock the context and execute a closure with it.
|
||||
///
|
||||
/// This is like [ContextWrapper::with_inner] but uses
|
||||
/// [failure::Error] as error type. This allows you to write a
|
||||
/// closure which could produce many errors, use the `?` operator
|
||||
/// to return them and handle them all as the return of this call.
|
||||
fn try_inner<T, F>(&self, ctxfn: F) -> Result<T, failure::Error>
|
||||
where
|
||||
F: FnOnce(&Context) -> Result<T, failure::Error>,
|
||||
{
|
||||
let guard = self.inner.read().unwrap();
|
||||
match guard.as_ref() {
|
||||
Some(ref ctx) => Ok(ctxfn(ctx)),
|
||||
None => {
|
||||
unsafe { self.error("context not open") };
|
||||
Err(())
|
||||
}
|
||||
Some(ref ctx) => ctxfn(ctx),
|
||||
None => Err(failure::err_msg("context not open")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +351,7 @@ pub unsafe extern "C" fn dc_set_config(
|
||||
})
|
||||
.unwrap_or(0),
|
||||
Err(_) => {
|
||||
ffi_context.error("dc_set_config(): invalid key");
|
||||
ffi_context.warning("dc_set_config(): invalid key");
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -361,7 +372,7 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
.with_inner(|ctx| ctx.get_config(key).unwrap_or_default().strdup())
|
||||
.unwrap_or_else(|_| "".strdup()),
|
||||
Err(_) => {
|
||||
ffi_context.error("dc_get_config(): invalid key");
|
||||
ffi_context.warning("dc_get_config(): invalid key");
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
@@ -396,11 +407,33 @@ pub unsafe extern "C" fn dc_set_stock_translation(
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_config_from_qr(
|
||||
context: *mut dc_context_t,
|
||||
qr: *mut libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || qr.is_null() {
|
||||
eprintln!("ignoring careless call to dc_set_config_from_qr");
|
||||
return 0;
|
||||
}
|
||||
let qr = to_string_lossy(qr);
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| match qr::set_config_from_qr(ctx, &qr) {
|
||||
Ok(()) => 1,
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed to create account from QR code: {}", err);
|
||||
0
|
||||
}
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_info()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
let guard = ffi_context.inner.read().unwrap();
|
||||
@@ -455,9 +488,7 @@ pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
|
||||
return;
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| configure::configure(ctx))
|
||||
.unwrap_or(())
|
||||
ffi_context.with_inner(|ctx| ctx.configure()).unwrap_or(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -468,7 +499,7 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| configure::dc_is_configured(ctx) as libc::c_int)
|
||||
.with_inner(|ctx| ctx.is_configured() as libc::c_int)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -665,6 +696,35 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
|
||||
.unwrap_or(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
context: *mut dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
public_data: *const libc::c_char,
|
||||
secret_data: *const libc::c_char,
|
||||
) -> i32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_preconfigure_keypair()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.try_inner(|ctx| {
|
||||
let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let public = key::SignedPublicKey::from_base64(&to_string_lossy(public_data))?;
|
||||
let secret = key::SignedSecretKey::from_base64(&to_string_lossy(secret_data))?;
|
||||
let keypair = key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
secret,
|
||||
};
|
||||
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default)?;
|
||||
Ok(1)
|
||||
})
|
||||
.log_err(ffi_context, "Failed to save keypair")
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_chatlist(
|
||||
context: *mut dc_context_t,
|
||||
@@ -708,7 +768,7 @@ pub unsafe extern "C" fn dc_create_chat_by_msg_id(context: *mut dc_context_t, ms
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
chat::create_by_msg_id(ctx, MsgId::new(msg_id))
|
||||
.log_err(ctx, "Failed to create chat from msg_id")
|
||||
.log_err(ffi_context, "Failed to create chat from msg_id")
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
@@ -728,7 +788,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
chat::create_by_contact_id(ctx, contact_id)
|
||||
.log_err(ctx, "Failed to create chat from contact_id")
|
||||
.log_err(ffi_context, "Failed to create chat from contact_id")
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
@@ -748,7 +808,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
chat::get_by_contact_id(ctx, contact_id)
|
||||
.log_err(ctx, "Failed to get chat for contact_id")
|
||||
.log_err(ffi_context, "Failed to get chat for contact_id")
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
@@ -836,7 +896,7 @@ pub unsafe extern "C" fn dc_set_draft(
|
||||
Some(&mut ffi_msg.message)
|
||||
};
|
||||
ffi_context
|
||||
.with_inner(|ctx| chat::set_draft(ctx, ChatId::new(chat_id), msg))
|
||||
.with_inner(|ctx| ChatId::new(chat_id).set_draft(ctx, msg))
|
||||
.unwrap_or(())
|
||||
}
|
||||
|
||||
@@ -911,7 +971,7 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32)
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| match chat::get_draft(ctx, ChatId::new(chat_id)) {
|
||||
.with_inner(|ctx| match ChatId::new(chat_id).get_draft(ctx) {
|
||||
Ok(Some(draft)) => {
|
||||
let ffi_msg = MessageWrapper {
|
||||
context,
|
||||
@@ -966,7 +1026,7 @@ pub unsafe extern "C" fn dc_get_msg_cnt(context: *mut dc_context_t, chat_id: u32
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| chat::get_msg_cnt(ctx, ChatId::new(chat_id)) as libc::c_int)
|
||||
.with_inner(|ctx| ChatId::new(chat_id).get_msg_cnt(ctx) as libc::c_int)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -981,7 +1041,26 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| chat::get_fresh_msg_cnt(ctx, ChatId::new(chat_id)) as libc::c_int)
|
||||
.with_inner(|ctx| ChatId::new(chat_id).get_fresh_msg_cnt(ctx) as libc::c_int)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
|
||||
context: *mut dc_context_t,
|
||||
from_server: libc::c_int,
|
||||
seconds: i64,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || seconds < 0 {
|
||||
eprintln!("ignoring careless call to dc_estimate_deletion_cnt()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
message::estimate_deletion_cnt(ctx, from_server != 0, seconds).unwrap_or(0)
|
||||
as libc::c_int
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -1017,7 +1096,7 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
chat::marknoticed_chat(ctx, ChatId::new(chat_id))
|
||||
.log_err(ctx, "Failed marknoticed chat")
|
||||
.log_err(ffi_context, "Failed marknoticed chat")
|
||||
.unwrap_or(())
|
||||
})
|
||||
.unwrap_or(())
|
||||
@@ -1033,7 +1112,7 @@ pub unsafe extern "C" fn dc_marknoticed_all_chats(context: *mut dc_context_t) {
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
chat::marknoticed_all_chats(ctx)
|
||||
.log_err(ctx, "Failed marknoticed all chats")
|
||||
.log_err(ffi_context, "Failed marknoticed all chats")
|
||||
.unwrap_or(())
|
||||
})
|
||||
.unwrap_or(())
|
||||
@@ -1126,27 +1205,32 @@ pub unsafe extern "C" fn dc_get_next_media(
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_archive_chat(
|
||||
pub unsafe extern "C" fn dc_set_chat_visibility(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
archive: libc::c_int,
|
||||
) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_archive_chat()");
|
||||
eprintln!("ignoring careless call to dc_set_chat_visibility()");
|
||||
return;
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
let archive = if archive == 0 {
|
||||
false
|
||||
} else if archive == 1 {
|
||||
true
|
||||
} else {
|
||||
return;
|
||||
let visibility = match archive {
|
||||
0 => ChatVisibility::Normal,
|
||||
1 => ChatVisibility::Archived,
|
||||
2 => ChatVisibility::Pinned,
|
||||
_ => {
|
||||
ffi_context.warning(
|
||||
"ignoring careless call to dc_set_chat_visibility(): unknown archived state",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
chat::archive(ctx, ChatId::new(chat_id), archive)
|
||||
.log_err(ctx, "Failed archive chat")
|
||||
ChatId::new(chat_id)
|
||||
.set_visibility(ctx, visibility)
|
||||
.log_err(ffi_context, "Failed setting chat visibility")
|
||||
.unwrap_or(())
|
||||
})
|
||||
.unwrap_or(())
|
||||
@@ -1161,8 +1245,9 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
chat::delete(ctx, ChatId::new(chat_id))
|
||||
.log_err(ctx, "Failed chat delete")
|
||||
ChatId::new(chat_id)
|
||||
.delete(ctx)
|
||||
.log_err(ffi_context, "Failed chat delete")
|
||||
.unwrap_or(())
|
||||
})
|
||||
.unwrap_or(())
|
||||
@@ -1249,7 +1334,7 @@ pub unsafe extern "C" fn dc_create_group_chat(
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
chat::create_group_chat(ctx, verified, to_string_lossy(name))
|
||||
.log_err(ctx, "Failed to create group chat")
|
||||
.log_err(ffi_context, "Failed to create group chat")
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
@@ -1351,6 +1436,37 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_chat_mute_duration(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
duration: i64,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_set_chat_mute_duration()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
let muteDuration = match duration {
|
||||
0 => MuteDuration::NotMuted,
|
||||
-1 => MuteDuration::Forever,
|
||||
n if n > 0 => MuteDuration::Until(SystemTime::now() + Duration::from_secs(duration as u64)),
|
||||
_ => {
|
||||
ffi_context.warning(
|
||||
"dc_chat_set_mute_duration(): Can not use negative duration other than -1",
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
chat::set_muted(ctx, ChatId::new(chat_id), muteDuration)
|
||||
.map(|_| 1)
|
||||
.unwrap_or_log_default(ctx, "Failed to set mute duration")
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_msg_info(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1358,7 +1474,7 @@ pub unsafe extern "C" fn dc_get_msg_info(
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_msg_info()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
@@ -1753,7 +1869,9 @@ pub unsafe extern "C" fn dc_imex_has_backup(
|
||||
.with_inner(|ctx| match imex::has_backup(ctx, to_string_lossy(dir)) {
|
||||
Ok(res) => res.strdup(),
|
||||
Err(err) => {
|
||||
error!(ctx, "dc_imex_has_backup: {}", err);
|
||||
// do not bubble up error to the user,
|
||||
// the ui will expect that the file does not exist or cannot be accessed
|
||||
warn!(ctx, "dc_imex_has_backup: {}", err);
|
||||
ptr::null_mut()
|
||||
}
|
||||
})
|
||||
@@ -1955,7 +2073,9 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
|
||||
}
|
||||
let ffi_context = &*context;
|
||||
ffi_context
|
||||
.with_inner(|ctx| location::delete_all(ctx).log_err(ctx, "Failed to delete locations"))
|
||||
.with_inner(|ctx| {
|
||||
location::delete_all(ctx).log_err(ffi_context, "Failed to delete locations")
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -2305,7 +2425,7 @@ pub unsafe extern "C" fn dc_chat_get_type(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
pub unsafe extern "C" fn dc_chat_get_name(chat: *mut dc_chat_t) -> *mut libc::c_char {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_get_name()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.get_name().strdup()
|
||||
@@ -2354,13 +2474,17 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_get_archived(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
pub unsafe extern "C" fn dc_chat_get_visibility(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_get_archived()");
|
||||
eprintln!("ignoring careless call to dc_chat_get_visibility()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.is_archived() as libc::c_int
|
||||
match ffi_chat.chat.visibility {
|
||||
ChatVisibility::Normal => 0,
|
||||
ChatVisibility::Archived => 1,
|
||||
ChatVisibility::Pinned => 2,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2423,6 +2547,37 @@ pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> l
|
||||
ffi_chat.chat.is_sending_locations() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_muted(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_is_muted()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.is_muted() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_get_remaining_mute_duration(chat: *mut dc_chat_t) -> i64 {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_get_remaining_mute_duration()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
if !ffi_chat.chat.is_muted() {
|
||||
return 0;
|
||||
}
|
||||
// If the chat was muted to before the epoch, it is not muted.
|
||||
match ffi_chat.chat.mute_duration {
|
||||
MuteDuration::NotMuted => 0,
|
||||
MuteDuration::Forever => -1,
|
||||
MuteDuration::Until(when) => when
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_get_info_json(
|
||||
context: *mut dc_context_t,
|
||||
@@ -2592,7 +2747,7 @@ pub unsafe extern "C" fn dc_msg_get_sort_timestamp(msg: *mut dc_msg_t) -> i64 {
|
||||
pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_text()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_text().unwrap_or_default().strdup()
|
||||
@@ -2611,8 +2766,7 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
|
||||
ffi_msg
|
||||
.message
|
||||
.get_file(ctx)
|
||||
.and_then(|p| p.to_c_string().ok())
|
||||
.map(|cs| dc_strdup(cs.as_ptr()))
|
||||
.map(|p| p.strdup())
|
||||
.unwrap_or_else(|| "".strdup())
|
||||
})
|
||||
.unwrap_or_else(|_| "".strdup())
|
||||
@@ -2622,7 +2776,7 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
|
||||
pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_filename()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_filename().unwrap_or_default().strdup()
|
||||
@@ -2632,13 +2786,13 @@ pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c
|
||||
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_filemime()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
if let Some(x) = ffi_msg.message.get_filemime() {
|
||||
x.strdup()
|
||||
} else {
|
||||
dc_strdup(ptr::null())
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2962,7 +3116,7 @@ pub unsafe extern "C" fn dc_contact_get_id(contact: *mut dc_contact_t) -> u32 {
|
||||
pub unsafe extern "C" fn dc_contact_get_addr(contact: *mut dc_contact_t) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_addr()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_addr().strdup()
|
||||
@@ -2972,7 +3126,7 @@ pub unsafe extern "C" fn dc_contact_get_addr(contact: *mut dc_contact_t) -> *mut
|
||||
pub unsafe extern "C" fn dc_contact_get_name(contact: *mut dc_contact_t) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_name()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_name().strdup()
|
||||
@@ -2984,7 +3138,7 @@ pub unsafe extern "C" fn dc_contact_get_display_name(
|
||||
) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_display_name()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_display_name().strdup()
|
||||
@@ -2996,7 +3150,7 @@ pub unsafe extern "C" fn dc_contact_get_name_n_addr(
|
||||
) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_name_n_addr()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_name_n_addr().strdup()
|
||||
@@ -3008,7 +3162,7 @@ pub unsafe extern "C" fn dc_contact_get_first_name(
|
||||
) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_first_name()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_first_name().strdup()
|
||||
@@ -3091,7 +3245,7 @@ pub unsafe extern "C" fn dc_lot_get_text1(lot: *mut dc_lot_t) -> *mut libc::c_ch
|
||||
}
|
||||
|
||||
let lot = &*lot;
|
||||
strdup_opt(lot.get_text1())
|
||||
lot.get_text1().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3102,7 +3256,7 @@ pub unsafe extern "C" fn dc_lot_get_text2(lot: *mut dc_lot_t) -> *mut libc::c_ch
|
||||
}
|
||||
|
||||
let lot = &*lot;
|
||||
strdup_opt(lot.get_text2())
|
||||
lot.get_text2().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3154,11 +3308,16 @@ pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
||||
libc::free(s as *mut _)
|
||||
}
|
||||
|
||||
pub mod providers;
|
||||
|
||||
pub trait ResultExt<T, E> {
|
||||
trait ResultExt<T, E> {
|
||||
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T;
|
||||
fn log_err(self, context: &context::Context, message: &str) -> Result<T, E>;
|
||||
|
||||
/// Log a warning to a [ContextWrapper] for an [Err] result.
|
||||
///
|
||||
/// Does nothing for an [Ok].
|
||||
///
|
||||
/// You can do this as soon as the wrapper exists, it does not
|
||||
/// have to be open (which is required for the `warn!()` macro).
|
||||
fn log_err(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E>;
|
||||
}
|
||||
|
||||
impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
|
||||
@@ -3172,22 +3331,17 @@ impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
|
||||
}
|
||||
}
|
||||
|
||||
fn log_err(self, context: &context::Context, message: &str) -> Result<T, E> {
|
||||
fn log_err(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E> {
|
||||
self.map_err(|err| {
|
||||
warn!(context, "{}: {}", message, err);
|
||||
unsafe {
|
||||
wrapper.warning(&format!("{}: {}", message, err));
|
||||
}
|
||||
err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn strdup_opt(s: Option<impl AsRef<str>>) -> *mut libc::c_char {
|
||||
match s {
|
||||
Some(s) => s.as_ref().strdup(),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ResultNullableExt<T> {
|
||||
trait ResultNullableExt<T> {
|
||||
fn into_raw(self) -> *mut T;
|
||||
}
|
||||
|
||||
@@ -3210,3 +3364,68 @@ fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> V
|
||||
|
||||
msg_ids
|
||||
}
|
||||
|
||||
// dc_provider_t
|
||||
|
||||
#[no_mangle]
|
||||
pub type dc_provider_t = provider::Provider;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
context: *const dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
) -> *const dc_provider_t {
|
||||
if context.is_null() || addr.is_null() {
|
||||
eprintln!("ignoring careless call to dc_provider_new_from_email()");
|
||||
return ptr::null();
|
||||
}
|
||||
let addr = to_string_lossy(addr);
|
||||
match provider::get_provider_info(addr.as_str()) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_get_overview_page(
|
||||
provider: *const dc_provider_t,
|
||||
) -> *mut libc::c_char {
|
||||
if provider.is_null() {
|
||||
eprintln!("ignoring careless call to dc_provider_get_overview_page()");
|
||||
return "".strdup();
|
||||
}
|
||||
let provider = &*provider;
|
||||
provider.overview_page.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_get_before_login_hint(
|
||||
provider: *const dc_provider_t,
|
||||
) -> *mut libc::c_char {
|
||||
if provider.is_null() {
|
||||
eprintln!("ignoring careless call to dc_provider_get_before_login_hint()");
|
||||
return "".strdup();
|
||||
}
|
||||
let provider = &*provider;
|
||||
provider.before_login_hint.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> libc::c_int {
|
||||
if provider.is_null() {
|
||||
eprintln!("ignoring careless call to dc_provider_get_status()");
|
||||
return 0;
|
||||
}
|
||||
let provider = &*provider;
|
||||
provider.status as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
|
||||
if provider.is_null() {
|
||||
eprintln!("ignoring careless call to dc_provider_unref()");
|
||||
return;
|
||||
}
|
||||
// currently, there is nothing to free, the provider info is a static object.
|
||||
// this may change once we start localizing string.
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
extern crate deltachat_provider_database;
|
||||
|
||||
use std::ptr;
|
||||
|
||||
use crate::string::{to_string_lossy, StrExt};
|
||||
use deltachat_provider_database::StatusState;
|
||||
|
||||
#[no_mangle]
|
||||
pub type dc_provider_t = deltachat_provider_database::Provider;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_new_from_domain(
|
||||
domain: *const libc::c_char,
|
||||
) -> *const dc_provider_t {
|
||||
match deltachat_provider_database::get_provider_info(&to_string_lossy(domain)) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
email: *const libc::c_char,
|
||||
) -> *const dc_provider_t {
|
||||
let email = to_string_lossy(email);
|
||||
let domain = deltachat_provider_database::get_domain_from_email(&email);
|
||||
match deltachat_provider_database::get_provider_info(domain) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null(),
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! null_guard {
|
||||
($context:tt) => {
|
||||
if $context.is_null() {
|
||||
return ptr::null_mut() as *mut libc::c_char;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_get_overview_page(
|
||||
provider: *const dc_provider_t,
|
||||
) -> *mut libc::c_char {
|
||||
null_guard!(provider);
|
||||
format!(
|
||||
"{}/{}",
|
||||
deltachat_provider_database::PROVIDER_OVERVIEW_URL,
|
||||
(*provider).overview_page
|
||||
)
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_get_name(provider: *const dc_provider_t) -> *mut libc::c_char {
|
||||
null_guard!(provider);
|
||||
(*provider).name.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_get_markdown(
|
||||
provider: *const dc_provider_t,
|
||||
) -> *mut libc::c_char {
|
||||
null_guard!(provider);
|
||||
(*provider).markdown.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_get_status_date(
|
||||
provider: *const dc_provider_t,
|
||||
) -> *mut libc::c_char {
|
||||
null_guard!(provider);
|
||||
(*provider).status.date.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> u32 {
|
||||
if provider.is_null() {
|
||||
return 0;
|
||||
}
|
||||
match (*provider).status.state {
|
||||
StatusState::OK => 1,
|
||||
StatusState::PREPARATION => 2,
|
||||
StatusState::BROKEN => 3,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_unref(_provider: *const dc_provider_t) {}
|
||||
|
||||
// TODO expose general provider overview url?
|
||||
@@ -1,5 +1,6 @@
|
||||
use failure::Fail;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ptr;
|
||||
|
||||
/// Duplicates a string
|
||||
///
|
||||
@@ -8,7 +9,7 @@ use std::ffi::{CStr, CString};
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,norun
|
||||
/// use deltachat::dc_tools::{dc_strdup, to_string_lossy};
|
||||
/// use crate::string::{dc_strdup, to_string_lossy};
|
||||
/// unsafe {
|
||||
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
|
||||
/// let str_a_copy = dc_strdup(str_a);
|
||||
@@ -16,7 +17,7 @@ use std::ffi::{CStr, CString};
|
||||
/// assert_ne!(str_a, str_a_copy);
|
||||
/// }
|
||||
/// ```
|
||||
pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
let ret: *mut libc::c_char;
|
||||
if !s.is_null() {
|
||||
ret = libc::strdup(s);
|
||||
@@ -31,7 +32,7 @@ pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||
|
||||
/// Error type for the [OsStrExt] trait
|
||||
#[derive(Debug, Fail, PartialEq)]
|
||||
pub enum CStringError {
|
||||
pub(crate) enum CStringError {
|
||||
/// The string contains an interior null byte
|
||||
#[fail(display = "String contains an interior null byte")]
|
||||
InteriorNullByte,
|
||||
@@ -65,7 +66,7 @@ pub enum CStringError {
|
||||
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
|
||||
/// }
|
||||
/// ```
|
||||
pub trait OsStrExt {
|
||||
pub(crate) trait OsStrExt {
|
||||
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
|
||||
///
|
||||
/// This is useful to convert e.g. a [std::path::Path] to
|
||||
@@ -130,27 +131,28 @@ fn os_str_to_c_string_unicode(
|
||||
}
|
||||
|
||||
/// Convenience methods/associated functions for working with [CString]
|
||||
///
|
||||
/// This is helps transitioning from unsafe code.
|
||||
pub trait CStringExt {
|
||||
/// Create a new [CString], yolo style
|
||||
trait CStringExt {
|
||||
/// Create a new [CString], best effort
|
||||
///
|
||||
/// This unwrap the result, panicking when there are embedded NULL
|
||||
/// bytes.
|
||||
fn yolo<T: Into<Vec<u8>>>(t: T) -> CString {
|
||||
CString::new(t).expect("String contains null byte, can not be CString")
|
||||
/// Like the [to_string_lossy] this doesn't give up in the face of
|
||||
/// bad input (embedded null bytes in this case) instead it does
|
||||
/// the best it can by stripping the embedded null bytes.
|
||||
fn new_lossy<T: Into<Vec<u8>>>(t: T) -> CString {
|
||||
let mut s = t.into();
|
||||
s.retain(|&c| c != 0);
|
||||
CString::new(s).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl CStringExt for CString {}
|
||||
|
||||
/// Convenience methods to make transitioning from raw C strings easier.
|
||||
/// Convenience methods to turn strings into C strings.
|
||||
///
|
||||
/// To interact with (legacy) C APIs we often need to convert from
|
||||
/// Rust strings to raw C strings. This can be clumsy to do correctly
|
||||
/// and the compiler sometimes allows it in an unsafe way. These
|
||||
/// methods make it more succinct and help you get it right.
|
||||
pub trait StrExt {
|
||||
pub(crate) trait Strdup {
|
||||
/// Allocate a new raw C `*char` version of this string.
|
||||
///
|
||||
/// This allocates a new raw C string which must be freed using
|
||||
@@ -167,14 +169,52 @@ pub trait StrExt {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> StrExt for T {
|
||||
impl<T: AsRef<str>> Strdup for T {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = CString::yolo(self.as_ref());
|
||||
let tmp = CString::new_lossy(self.as_ref());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||
// We can not implement for AsRef<OsStr> because we already implement
|
||||
// AsRev<str> and this conflicts. So implement for Path directly.
|
||||
impl Strdup for std::path::Path {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience methods to turn optional strings into C strings.
|
||||
///
|
||||
/// This is the same as the [Strdup] trait but a different trait name
|
||||
/// to work around the type system not allowing to implement [Strdup]
|
||||
/// for `Option<impl Strdup>` When we already have an [Strdup] impl
|
||||
/// for `AsRef<&str>`.
|
||||
///
|
||||
/// When the [Option] is [Option::Some] this behaves just like
|
||||
/// [Strdup::strdup], when it is [Option::None] a null pointer is
|
||||
/// returned.
|
||||
pub(crate) trait OptStrdup {
|
||||
/// Allocate a new raw C `*char` version of this string, or NULL.
|
||||
///
|
||||
/// See [Strdup::strdup] for details.
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> OptStrdup for Option<T> {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
match self {
|
||||
Some(s) => {
|
||||
let tmp = CString::new_lossy(s.as_ref());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||
if s.is_null() {
|
||||
return "".into();
|
||||
}
|
||||
@@ -184,7 +224,7 @@ pub fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||
cstr.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
if s.is_null() {
|
||||
return None;
|
||||
}
|
||||
@@ -205,7 +245,7 @@ pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
///
|
||||
/// [Path]: std::path::Path
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
unsafe {
|
||||
@@ -217,7 +257,7 @@ pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
|
||||
// as_path() implementation for windows, documented above.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
as_path_unicode(s)
|
||||
}
|
||||
|
||||
@@ -324,8 +364,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cstring_yolo() {
|
||||
assert_eq!(CString::new("hello").unwrap(), CString::yolo("hello"));
|
||||
fn test_cstring_new_lossy() {
|
||||
assert!(CString::new("hel\x00lo").is_err());
|
||||
assert!(CString::new(String::from("hel\x00o")).is_err());
|
||||
let r = CString::new("hello").unwrap();
|
||||
assert_eq!(CString::new_lossy("hello"), r);
|
||||
assert_eq!(CString::new_lossy("hel\x00lo"), r);
|
||||
assert_eq!(CString::new_lossy(String::from("hello")), r);
|
||||
assert_eq!(CString::new_lossy(String::from("hel\x00lo")), r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -347,4 +393,19 @@ mod tests {
|
||||
assert_eq!(cmp, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strdup_opt_string() {
|
||||
unsafe {
|
||||
let s = Some("hello");
|
||||
let c = s.strdup();
|
||||
let cmp = strcmp(c, b"hello\x00" as *const u8 as *const libc::c_char);
|
||||
free(c as *mut libc::c_void);
|
||||
assert_eq!(cmp, 0);
|
||||
|
||||
let s: Option<&str> = None;
|
||||
let c = s.strdup();
|
||||
assert_eq!(c, ptr::null_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_derive"
|
||||
version = "1.0.0-beta.22"
|
||||
version = "2.0.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
||||
126
draft/group-sync.rst
Normal file
126
draft/group-sync.rst
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
Problem: missing eventual group consistency
|
||||
--------------------------------------------
|
||||
|
||||
If group members are concurrently adding new members,
|
||||
the new members will miss each other's additions, example:
|
||||
|
||||
- Alice and Bob are in a two-member group
|
||||
|
||||
- Alice adds Carol, concurrently Bob adds Doris
|
||||
|
||||
- Carol will see a three-member group (Alice, Bob, Carol),
|
||||
Doris will see a different three-member group (Alice, Bob, Doris),
|
||||
and only Alice and Bob will have all four members.
|
||||
|
||||
Note that for verified groups any mitigation mechanism likely
|
||||
needs to make all clients to know who originally added a member.
|
||||
|
||||
|
||||
solution: memorize+attach (possible encrypted) chat-meta mime messages
|
||||
----------------------------------------------------------------------
|
||||
|
||||
For reference, please see https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members how MemberAdded/Removed messages are shaped.
|
||||
|
||||
|
||||
- All Chat-Group-Member-Added/Removed messages are recorded in their
|
||||
full raw (signed and encrypted) mime-format in the DB
|
||||
|
||||
- If an incoming member-add/member-delete messages has a member list
|
||||
which is, apart from the added/removed member, not consistent
|
||||
with our own view, broadcast a "Chat-Group-Member-Correction" message to
|
||||
all members, attaching the original added/removed mime-message for all mismatching
|
||||
contacts. If we have no relevant add/del information, don't send a
|
||||
correction message out.
|
||||
|
||||
- Upong receiving added/removed attachments we don't do the
|
||||
check_consistency+correction message dance.
|
||||
This avoids recursion problems and hard-to-reason-about chatter.
|
||||
|
||||
Notes:
|
||||
|
||||
- mechanism works for both encrypted and unencrypted add/del messages
|
||||
|
||||
- we already have a "mime_headers" column in the DB for each incoming message.
|
||||
We could extend it to also include the payload and store mime unconditionally
|
||||
for member-added/removed messages.
|
||||
|
||||
- multiple member-added/removed messages can be attached in a single
|
||||
correction message
|
||||
|
||||
- it is minimal on the number of overall messages to reach group consistency
|
||||
(best-case: no extra messages, the ABCD case above: max two extra messages)
|
||||
|
||||
- somewhat backward compatible: older clients will probably ignore
|
||||
messages which are signed by someone who is not the outer From-address.
|
||||
|
||||
- the correction-protocol also helps with dropped messages. If a member
|
||||
did not see a member-added/removed message, the next member add/removed
|
||||
message in the group will likely heal group consistency for this member.
|
||||
|
||||
- we can quite easily extend the mechanism to also provide the group-avatar or
|
||||
other meta-information.
|
||||
|
||||
Discussions of variants
|
||||
++++++++++++++++++++++++
|
||||
|
||||
- instead of acting on MemberAdded/Removed message we could send
|
||||
corrections for any received message that addresses inconsistent group members but
|
||||
a) this would delay group-membership healing
|
||||
b) could lead to a lot of members sending corrections
|
||||
|
||||
- instead of broadcasting correction messages we could only send it to
|
||||
the sender of the inconsistent member-added/removed message.
|
||||
A receiver of such a correction message would then need to forward
|
||||
the message to the members it thinks also have an inconsistent view.
|
||||
This sounds complicated and error-prone. Concretely, if Alice
|
||||
receives Bob's "Member-added: Doris" message, then Alice
|
||||
broadcasting the correction message with "Member-added: Carol"
|
||||
would reach all four members, healing group consistency in one step.
|
||||
If Bob meanwhile receives Alice's "Member-Added: Carol" message,
|
||||
Bob would broadcast a correction message to all four members as well.
|
||||
(Imagine a situation where Alice/Bob added Carol/Doris
|
||||
while both being in an offline or bad-connection situation).
|
||||
|
||||
|
||||
solution2: repeat member-added/removed messages
|
||||
---------------------------------------------------
|
||||
|
||||
Introduce a new Chat-Group-Member-Changed header and deprecate Chat-Group-Member-Added/Removed
|
||||
but keep sending out the old headers until the new protocol is sufficiently deployed.
|
||||
|
||||
The new Chat-Group-Member-Changed header contains a Time-to-Live number (TTL)
|
||||
which controls repetition of the signed "add/del e-mail address" payload.
|
||||
|
||||
Example::
|
||||
|
||||
Chat-Group-Member-Changed: TTL add "somedisplayname" someone@example.org
|
||||
owEBYQGe/pANAwACAY47A6J5t3LWAcsxYgBeTQypYWRkICJzb21lZGlzcGxheW5h
|
||||
bWUiIHNvbWVvbmVAZXhhbXBsZS5vcmcgCokBHAQAAQIABgUCXk0MqQAKCRCOOwOi
|
||||
ebdy1hfRB/wJ74tgFQulicthcv9n+ZsqzwOtBKMEVIHqJCzzDB/Hg/2z8ogYoZNR
|
||||
iUKKrv3Y1XuFvdKyOC+wC/unXAWKFHYzY6Tv6qDp6r+amt+ad+8Z02q53h9E55IP
|
||||
FUBdq2rbS8hLGjQB+mVRowYrUACrOqGgNbXMZjQfuV7fSc7y813OsCQgi3tjstup
|
||||
b+uduVzxCp3PChGhcZPs3iOGCnQvSB8VAaLGMWE2d7nTo/yMQ0Jx69x5qwfXogTk
|
||||
mTt5rOJyrosbtf09TMKFzGgtqBcEqHLp3+mQpZQ+WHUKAbsRa8Jc9DOUOSKJ8SNM
|
||||
clKdskprY+4LY0EBwLD3SQ7dPkTITCRD
|
||||
=P6GG
|
||||
|
||||
TTL is set to "2" on an initial Chat-Group-Member-Changed add/del message.
|
||||
Receivers will apply the add/del change to the group-membership,
|
||||
decrease the TTL by 1, and if TTL>0 re-sent the header.
|
||||
|
||||
The "add|del e-mail address" payload is pgp-signed and repeated verbatim.
|
||||
This allows to propagate, in a cryptographically secured way,
|
||||
who added a member. This is particularly important for allowing
|
||||
to show in verified groups who added a member (planned).
|
||||
|
||||
Disadvantage to solution 1:
|
||||
|
||||
- requires to specify encoding and precise rules for what/how is signed.
|
||||
|
||||
- causes O(N^2) extra messages
|
||||
|
||||
- Not easily extendable for other things (without introducing a new
|
||||
header / encoding)
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use deltachat::chat::{self, Chat, ChatId};
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::config;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::dc_receive_imf::*;
|
||||
use deltachat::dc_tools::*;
|
||||
use deltachat::error::Error;
|
||||
use deltachat::export_chat::{export_chat, pack_exported_chat};
|
||||
use deltachat::imex::*;
|
||||
use deltachat::job::*;
|
||||
use deltachat::location;
|
||||
@@ -19,6 +20,7 @@ use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::sql;
|
||||
use deltachat::Event;
|
||||
use deltachat::{config, provider};
|
||||
|
||||
/// Reset database tables.
|
||||
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||
@@ -94,7 +96,7 @@ fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
|
||||
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), Error> {
|
||||
let data = dc_read_file(context, filename)?;
|
||||
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", 0, 0) {
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false) {
|
||||
println!("dc_receive_imf errored: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
@@ -371,7 +373,10 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
listmedia\n\
|
||||
archive <chat-id>\n\
|
||||
unarchive <chat-id>\n\
|
||||
pin <chat-id>\n\
|
||||
unpin <chat-id>\n\
|
||||
delchat <chat-id>\n\
|
||||
export-chat <chat-id>\n\
|
||||
===========================Message commands==\n\
|
||||
listmsgs <query>\n\
|
||||
msginfo <msg-id>\n\
|
||||
@@ -392,8 +397,10 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
getqr [<chat-id>]\n\
|
||||
getbadqr\n\
|
||||
checkqr <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
event <event-id to test>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
|
||||
clear -- clear screen\n\
|
||||
exit or quit\n\
|
||||
@@ -510,14 +517,19 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
for i in (0..cnt).rev() {
|
||||
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
|
||||
println!(
|
||||
"{}#{}: {} [{} fresh]",
|
||||
"{}#{}: {} [{} fresh] {}",
|
||||
chat_prefix(&chat),
|
||||
chat.get_id(),
|
||||
chat.get_name(),
|
||||
chat::get_fresh_msg_cnt(context, chat.get_id()),
|
||||
chat.get_id().get_fresh_msg_cnt(context),
|
||||
match chat.visibility {
|
||||
ChatVisibility::Normal => "",
|
||||
ChatVisibility::Archived => "📦",
|
||||
ChatVisibility::Pinned => "📌",
|
||||
},
|
||||
);
|
||||
let lot = chatlist.get_summary(context, i, Some(&chat));
|
||||
let statestr = if chat.is_archived() {
|
||||
let statestr = if chat.visibility == ChatVisibility::Archived {
|
||||
" [Archived]"
|
||||
} else {
|
||||
match lot.get_state() {
|
||||
@@ -598,14 +610,11 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
},
|
||||
);
|
||||
log_msglist(context, &msglist)?;
|
||||
if let Some(draft) = chat::get_draft(context, sel_chat.get_id())? {
|
||||
if let Some(draft) = sel_chat.get_id().get_draft(context)? {
|
||||
log_msg(context, "Draft", &draft);
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} messages.",
|
||||
chat::get_msg_cnt(context, sel_chat.get_id())
|
||||
);
|
||||
println!("{} messages.", sel_chat.get_id().get_msg_cnt(context));
|
||||
chat::marknoticed_chat(context, sel_chat.get_id())?;
|
||||
}
|
||||
"createchat" => {
|
||||
@@ -801,14 +810,14 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
if !arg1.is_empty() {
|
||||
let mut draft = Message::new(Viewtype::Text);
|
||||
draft.set_text(Some(arg1.to_string()));
|
||||
chat::set_draft(
|
||||
context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
Some(&mut draft),
|
||||
);
|
||||
sel_chat
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_id()
|
||||
.set_draft(context, Some(&mut draft));
|
||||
println!("Draft saved.");
|
||||
} else {
|
||||
chat::set_draft(context, sel_chat.as_ref().unwrap().get_id(), None);
|
||||
sel_chat.as_ref().unwrap().get_id().set_draft(context, None);
|
||||
println!("Draft deleted.");
|
||||
}
|
||||
}
|
||||
@@ -844,15 +853,41 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
}
|
||||
print!("\n");
|
||||
}
|
||||
"archive" | "unarchive" => {
|
||||
"archive" | "unarchive" | "pin" | "unpin" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
chat::archive(context, chat_id, arg0 == "archive")?;
|
||||
chat_id.set_visibility(
|
||||
context,
|
||||
match arg0 {
|
||||
"archive" => ChatVisibility::Archived,
|
||||
"unarchive" | "unpin" => ChatVisibility::Normal,
|
||||
"pin" => ChatVisibility::Pinned,
|
||||
_ => panic!("Unexpected command (This should never happen)"),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
"delchat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
chat::delete(context, chat_id)?;
|
||||
chat_id.delete(context)?;
|
||||
}
|
||||
"export-chat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = ChatId::new(arg1.parse()?);
|
||||
let res = export_chat(context, chat_id);
|
||||
println!("{:?}", res);
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
let destination_raw = context.get_blobdir().join(format!(
|
||||
"exported_{}_{}.zip",
|
||||
chat_id.to_u32(),
|
||||
timestamp
|
||||
));
|
||||
let destination = destination_raw.to_str().unwrap();
|
||||
let pack_res = pack_exported_chat(context, res, destination);
|
||||
println!("{:?} - destination: {}", pack_res, destination);
|
||||
}
|
||||
"msginfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
@@ -969,6 +1004,31 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
res.get_text2()
|
||||
);
|
||||
}
|
||||
"setqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
match set_config_from_qr(context, arg1) {
|
||||
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
||||
Err(err) => println!("Cannot set config from QR code: {:?}", err),
|
||||
}
|
||||
}
|
||||
"providerinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||
match provider::get_provider_info(arg1) {
|
||||
Some(info) => {
|
||||
println!("Information for provider belonging to {}:", arg1);
|
||||
println!("status: {}", info.status as u32);
|
||||
println!("before_login_hint: {}", info.before_login_hint);
|
||||
println!("after_login_hint: {}", info.after_login_hint);
|
||||
println!("overview_page: {}", info.overview_page);
|
||||
for server in info.server.iter() {
|
||||
println!("server: {}:{}", server.hostname, server.port,);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("No information for provider belonging to {} found.", arg1);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: implement this again, unclear how to match this through though, without writing a parser.
|
||||
// "event" => {
|
||||
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
|
||||
@@ -990,6 +1050,16 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
bail!("Command failed.");
|
||||
}
|
||||
}
|
||||
"estimatedeletion" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
|
||||
let seconds = arg1.parse()?;
|
||||
let device_cnt = message::estimate_deletion_cnt(context, false, seconds)?;
|
||||
let server_cnt = message::estimate_deletion_cnt(context, true, seconds)?;
|
||||
println!(
|
||||
"estimated count of messages older than {} seconds:\non device: {}\non server: {}",
|
||||
seconds, device_cnt, server_cnt
|
||||
);
|
||||
}
|
||||
"emptyserver" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <flags> missing");
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::config;
|
||||
use deltachat::configure::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::job::*;
|
||||
use deltachat::oauth2::*;
|
||||
@@ -263,7 +262,7 @@ const DB_COMMANDS: [&str; 11] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 24] = [
|
||||
const CHAT_COMMANDS: [&str; 27] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
@@ -287,7 +286,10 @@ const CHAT_COMMANDS: [&str; 24] = [
|
||||
"listmedia",
|
||||
"archive",
|
||||
"unarchive",
|
||||
"pin",
|
||||
"unpin",
|
||||
"delchat",
|
||||
"export-chat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
||||
"listmsgs",
|
||||
@@ -307,8 +309,17 @@ const CONTACT_COMMANDS: [&str; 6] = [
|
||||
"delcontact",
|
||||
"cleanupcontacts",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 9] = [
|
||||
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
|
||||
const MISC_COMMANDS: [&str; 10] = [
|
||||
"getqr",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"event",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
"exit",
|
||||
"quit",
|
||||
"help",
|
||||
"estimatedeletion",
|
||||
];
|
||||
|
||||
impl Hinter for DcHelper {
|
||||
@@ -461,7 +472,7 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
|
||||
}
|
||||
"configure" => {
|
||||
start_threads(ctx.clone());
|
||||
configure(&ctx.read().unwrap());
|
||||
ctx.read().unwrap().configure();
|
||||
}
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
|
||||
|
||||
@@ -7,7 +7,6 @@ use tempfile::tempdir;
|
||||
use deltachat::chat;
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::config;
|
||||
use deltachat::configure::*;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::job::{
|
||||
@@ -21,13 +20,13 @@ fn cb(_ctx: &Context, event: Event) {
|
||||
|
||||
match event {
|
||||
Event::ConfigureProgress(progress) => {
|
||||
print!(" progress: {}\n", progress);
|
||||
println!(" progress: {}", progress);
|
||||
}
|
||||
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
|
||||
print!(" {}\n", msg);
|
||||
println!(" {}", msg);
|
||||
}
|
||||
_ => {
|
||||
print!("\n");
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +76,7 @@ fn main() {
|
||||
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
|
||||
.unwrap();
|
||||
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
|
||||
configure(&ctx);
|
||||
ctx.configure();
|
||||
|
||||
thread::sleep(duration);
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
0.800.0
|
||||
-------
|
||||
|
||||
- use latest core 1.25.0
|
||||
|
||||
- refine tests and some internal changes to core bindings
|
||||
|
||||
0.700.0
|
||||
---------
|
||||
|
||||
|
||||
@@ -52,20 +52,21 @@ Python packages before running the tests::
|
||||
pytest -v tests
|
||||
|
||||
|
||||
running "live" tests (experimental)
|
||||
-----------------------------------
|
||||
running "live" tests with temporary accounts
|
||||
---------------------------------------------
|
||||
|
||||
If you want to run "liveconfig" functional tests you can set
|
||||
``DCC_PY_LIVECONFIG`` to:
|
||||
``DCC_NEW_TMP_EMAIL`` to:
|
||||
|
||||
- a particular https-url that you can ask for from the delta
|
||||
chat devs.
|
||||
chat devs. This is implemented on the server side via
|
||||
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
|
||||
|
||||
- or the path of a file that contains two lines, each describing
|
||||
via "addr=... mail_pw=..." a test account login that will
|
||||
be used for the live tests.
|
||||
|
||||
With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real
|
||||
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
|
||||
e-mail accounts and run through all functional "liveconfig" tests.
|
||||
|
||||
|
||||
@@ -129,7 +130,7 @@ organization::
|
||||
|
||||
This docker image can be used to run tests and build Python wheels for all interpreters::
|
||||
|
||||
$ docker run -e DCC_PY_LIVECONFIG \
|
||||
$ docker run -e DCC_NEW_TMP_EMAIL \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps ci_scripts/run_all.sh
|
||||
|
||||
|
||||
4
python/doc/_static/custom.css
vendored
4
python/doc/_static/custom.css
vendored
@@ -15,3 +15,7 @@ div.globaltoc {
|
||||
img.logo {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'deltachat'
|
||||
copyright = u'2018, holger krekel and contributors'
|
||||
copyright = u'2020, holger krekel and contributors'
|
||||
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
deltachat python bindings
|
||||
=========================
|
||||
|
||||
The ``deltachat`` Python package provides two bindings for the core Rust-library
|
||||
of the https://delta.chat messaging ecosystem:
|
||||
The ``deltachat`` Python package provides two layers of bindings for the
|
||||
core Rust-library of the https://delta.chat messaging ecosystem:
|
||||
|
||||
- :doc:`api` is a high level interface to deltachat-core which aims
|
||||
to be memory safe and thoroughly tested through continous tox/pytest runs.
|
||||
|
||||
- :doc:`capi` is a lowlevel CFFI-binding to the previous
|
||||
`deltachat-core C-API <https://c.delta.chat>`_ (so far the Rust library
|
||||
replicates exactly the same C-level API).
|
||||
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
|
||||
<https://github.com/deltachat/deltachat-core-rust>`_.
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +27,6 @@ getting started
|
||||
links
|
||||
changelog
|
||||
api
|
||||
capi
|
||||
lapi
|
||||
|
||||
..
|
||||
|
||||
@@ -11,18 +11,15 @@ import sys
|
||||
if __name__ == "__main__":
|
||||
target = os.environ.get("DCC_RS_TARGET")
|
||||
if target is None:
|
||||
os.environ["DCC_RS_TARGET"] = target = "release"
|
||||
os.environ["DCC_RS_TARGET"] = target = "debug"
|
||||
if "DCC_RS_DEV" not in os.environ:
|
||||
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ["DCC_RS_DEV"] = dn
|
||||
|
||||
# build the core library in release + debug mode because
|
||||
# as of Nov 2019 rPGP generates RSA keys which take
|
||||
# prohibitively long for non-release installs
|
||||
os.environ["RUSTFLAGS"] = "-g"
|
||||
subprocess.check_call([
|
||||
"cargo", "build", "-p", "deltachat_ffi", "--" + target
|
||||
])
|
||||
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
|
||||
if target == 'release':
|
||||
cmd.append("--release")
|
||||
subprocess.check_call(cmd)
|
||||
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
|
||||
|
||||
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
|
||||
|
||||
@@ -18,7 +18,7 @@ def main():
|
||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||
long_description=long_description,
|
||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||
install_requires=['cffi>=1.0.0', 'six'],
|
||||
install_requires=['cffi>=1.0.0', 'pluggy'],
|
||||
packages=setuptools.find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||
|
||||
@@ -4,13 +4,9 @@ from __future__ import print_function
|
||||
import atexit
|
||||
import threading
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from array import array
|
||||
try:
|
||||
from queue import Queue, Empty
|
||||
except ImportError:
|
||||
from Queue import Queue, Empty
|
||||
from queue import Queue
|
||||
|
||||
import deltachat
|
||||
from . import const
|
||||
@@ -19,6 +15,8 @@ from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||
from .chat import Chat
|
||||
from .message import Message
|
||||
from .contact import Contact
|
||||
from .eventlogger import EventLogger
|
||||
from .hookspec import get_plugin_manager, hookimpl
|
||||
|
||||
|
||||
class Account(object):
|
||||
@@ -26,14 +24,13 @@ class Account(object):
|
||||
by the underlying deltachat core library. All public Account methods are
|
||||
meant to be memory-safe and return memory-safe objects.
|
||||
"""
|
||||
def __init__(self, db_path, logid=None, eventlogging=True, os_name=None, debug=True):
|
||||
def __init__(self, db_path, logid=None, os_name=None, debug=True):
|
||||
""" initialize account object.
|
||||
|
||||
:param db_path: a path to the account database. The database
|
||||
will be created if it doesn't exist.
|
||||
:param logid: an optional logging prefix that should be used with
|
||||
the default internal logging.
|
||||
:param eventlogging: if False no eventlogging and no context callback will be configured
|
||||
:param os_name: this will be put to the X-Mailer header in outgoing messages
|
||||
:param debug: turn on debug logging for events.
|
||||
"""
|
||||
@@ -41,19 +38,26 @@ class Account(object):
|
||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
|
||||
_destroy_dc_context,
|
||||
)
|
||||
if eventlogging:
|
||||
self._evlogger = EventLogger(self._dc_context, logid, debug)
|
||||
deltachat.set_context_callback(self._dc_context, self._process_event)
|
||||
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
|
||||
else:
|
||||
self._threads = IOThreads(self._dc_context)
|
||||
self._evlogger = EventLogger(self, logid, debug)
|
||||
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
|
||||
|
||||
# register event call back and initialize plugin system
|
||||
def _ll_event(ctx, evt_name, data1, data2):
|
||||
assert ctx == self._dc_context
|
||||
self.pluggy.hook.process_low_level_event(
|
||||
account=self, event_name=evt_name, data1=data1, data2=data2
|
||||
)
|
||||
|
||||
self.pluggy = get_plugin_manager()
|
||||
self.pluggy.register(self._evlogger)
|
||||
deltachat.set_context_callback(self._dc_context, _ll_event)
|
||||
|
||||
# open database
|
||||
if hasattr(db_path, "encode"):
|
||||
db_path = db_path.encode("utf8")
|
||||
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
||||
raise ValueError("Could not dc_open: {}".format(db_path))
|
||||
self._configkeys = self.get_config("sys.config_keys").split()
|
||||
self._imex_events = Queue()
|
||||
atexit.register(self.shutdown)
|
||||
|
||||
# def __del__(self):
|
||||
@@ -118,6 +122,18 @@ class Account(object):
|
||||
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def _preconfigure_keypair(self, addr, public, secret):
|
||||
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||
|
||||
In other words, you don't need this.
|
||||
"""
|
||||
res = lib.dc_preconfigure_keypair(self._dc_context,
|
||||
as_dc_charpointer(addr),
|
||||
as_dc_charpointer(public),
|
||||
as_dc_charpointer(secret))
|
||||
if res == 0:
|
||||
raise Exception("Failed to set key")
|
||||
|
||||
def configure(self, **kwargs):
|
||||
""" set config values and configure this account.
|
||||
|
||||
@@ -165,11 +181,6 @@ class Account(object):
|
||||
raise ValueError("no flags set")
|
||||
lib.dc_empty_server(self._dc_context, flags)
|
||||
|
||||
def get_infostring(self):
|
||||
""" return info of the configured account. """
|
||||
self.check_is_configured()
|
||||
return from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||
|
||||
def get_latest_backupfile(self, backupdir):
|
||||
""" return the latest backup file in a given directory.
|
||||
"""
|
||||
@@ -370,27 +381,12 @@ class Account(object):
|
||||
raise RuntimeError("found more than one new file")
|
||||
return export_files[0]
|
||||
|
||||
def _imex_events_clear(self):
|
||||
try:
|
||||
while True:
|
||||
self._imex_events.get_nowait()
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
def _export(self, path, imex_cmd):
|
||||
self._imex_events_clear()
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
if not self._threads.is_started():
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
files_written = []
|
||||
while True:
|
||||
ev = self._imex_events.get()
|
||||
if isinstance(ev, str):
|
||||
files_written.append(ev)
|
||||
elif isinstance(ev, bool):
|
||||
if not ev:
|
||||
raise ValueError("export failed, exp-files: {}".format(files_written))
|
||||
return files_written
|
||||
with ImexTracker(self) as imex_tracker:
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
if not self._threads.is_started():
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
return imex_tracker.wait_finish()
|
||||
|
||||
def import_self_keys(self, path):
|
||||
""" Import private keys found in the `path` directory.
|
||||
@@ -408,12 +404,11 @@ class Account(object):
|
||||
self._import(path, imex_cmd=12)
|
||||
|
||||
def _import(self, path, imex_cmd):
|
||||
self._imex_events_clear()
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
if not self._threads.is_started():
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
if not self._imex_events.get():
|
||||
raise ValueError("import from path '{}' failed".format(path))
|
||||
with ImexTracker(self) as imex_tracker:
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||
if not self._threads.is_started():
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
imex_tracker.wait_finish()
|
||||
|
||||
def initiate_key_transfer(self):
|
||||
"""return setup code after a Autocrypt setup message
|
||||
@@ -516,24 +511,7 @@ class Account(object):
|
||||
deltachat.clear_context_callback(self._dc_context)
|
||||
del self._dc_context
|
||||
atexit.unregister(self.shutdown)
|
||||
|
||||
def _process_event(self, ctx, evt_name, data1, data2):
|
||||
assert ctx == self._dc_context
|
||||
if hasattr(self, "_evlogger"):
|
||||
self._evlogger(evt_name, data1, data2)
|
||||
method = getattr(self, "on_" + evt_name.lower(), None)
|
||||
if method is not None:
|
||||
method(data1, data2)
|
||||
return 0
|
||||
|
||||
def on_dc_event_imex_progress(self, data1, data2):
|
||||
if data1 == 1000:
|
||||
self._imex_events.put(True)
|
||||
elif data1 == 0:
|
||||
self._imex_events.put(False)
|
||||
|
||||
def on_dc_event_imex_file_written(self, data1, data2):
|
||||
self._imex_events.put(data1)
|
||||
self.pluggy.unregister(self._evlogger)
|
||||
|
||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||
"""set a new location. It effects all chats where we currently
|
||||
@@ -550,6 +528,41 @@ class Account(object):
|
||||
raise ValueError("no chat is streaming locations")
|
||||
|
||||
|
||||
class ImexTracker:
|
||||
def __init__(self, account):
|
||||
self._imex_events = Queue()
|
||||
self.account = account
|
||||
|
||||
def __enter__(self):
|
||||
self.account.pluggy.register(self)
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.account.pluggy.unregister(self)
|
||||
|
||||
@hookimpl
|
||||
def process_low_level_event(self, account, event_name, data1, data2):
|
||||
# there could be multiple accounts instantiated
|
||||
if self.account is not account:
|
||||
return
|
||||
if event_name == "DC_EVENT_IMEX_PROGRESS":
|
||||
self._imex_events.put(data1)
|
||||
elif event_name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
||||
self._imex_events.put(data1)
|
||||
|
||||
def wait_finish(self, progress_timeout=60):
|
||||
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||
files_written = []
|
||||
while True:
|
||||
ev = self._imex_events.get(timeout=progress_timeout)
|
||||
if isinstance(ev, str):
|
||||
files_written.append(ev)
|
||||
elif ev == 0:
|
||||
raise ValueError("export failed, exp-files: {}".format(files_written))
|
||||
elif ev == 1000:
|
||||
return files_written
|
||||
|
||||
|
||||
class IOThreads:
|
||||
def __init__(self, dc_context, log_event=lambda *args: None):
|
||||
self._dc_context = dc_context
|
||||
@@ -578,6 +591,11 @@ class IOThreads:
|
||||
|
||||
def stop(self, wait=False):
|
||||
self._thread_quitflag = True
|
||||
|
||||
# Workaround for a race condition. Make sure that thread is
|
||||
# not in between checking for quitflag and entering idle.
|
||||
time.sleep(0.5)
|
||||
|
||||
lib.dc_interrupt_imap_idle(self._dc_context)
|
||||
lib.dc_interrupt_smtp_idle(self._dc_context)
|
||||
lib.dc_interrupt_mvbox_idle(self._dc_context)
|
||||
@@ -600,100 +618,31 @@ class IOThreads:
|
||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_jobs(self._dc_context)
|
||||
lib.dc_perform_mvbox_fetch(self._dc_context)
|
||||
lib.dc_perform_mvbox_idle(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
|
||||
|
||||
def sentbox_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_jobs(self._dc_context)
|
||||
lib.dc_perform_sentbox_fetch(self._dc_context)
|
||||
lib.dc_perform_sentbox_idle(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
|
||||
|
||||
def smtp_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_smtp_jobs(self._dc_context)
|
||||
lib.dc_perform_smtp_idle(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_smtp_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
|
||||
|
||||
|
||||
class EventLogger:
|
||||
_loglock = threading.RLock()
|
||||
|
||||
def __init__(self, dc_context, logid=None, debug=True):
|
||||
self._dc_context = dc_context
|
||||
self._event_queue = Queue()
|
||||
self._debug = debug
|
||||
if logid is None:
|
||||
logid = str(self._dc_context).strip(">").split()[-1]
|
||||
self.logid = logid
|
||||
self._timeout = None
|
||||
self.init_time = time.time()
|
||||
|
||||
def __call__(self, evt_name, data1, data2):
|
||||
self._log_event(evt_name, data1, data2)
|
||||
self._event_queue.put((evt_name, data1, data2))
|
||||
|
||||
def set_timeout(self, timeout):
|
||||
self._timeout = timeout
|
||||
|
||||
def consume_events(self, check_error=True):
|
||||
while not self._event_queue.empty():
|
||||
self.get()
|
||||
|
||||
def get(self, timeout=None, check_error=True):
|
||||
timeout = timeout or self._timeout
|
||||
ev = self._event_queue.get(timeout=timeout)
|
||||
if check_error and ev[0] == "DC_EVENT_ERROR":
|
||||
raise ValueError("{}({!r},{!r})".format(*ev))
|
||||
return ev
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
__tracebackhide__ = True
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
while 1:
|
||||
try:
|
||||
ev = self._event_queue.get(False)
|
||||
except Empty:
|
||||
break
|
||||
else:
|
||||
assert not rex.match(ev[0]), "event found {}".format(ev)
|
||||
|
||||
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
||||
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
while 1:
|
||||
ev = self.get(timeout=timeout, check_error=check_error)
|
||||
if rex.match(ev[0]):
|
||||
return ev
|
||||
|
||||
def get_info_matching(self, regex):
|
||||
rex = re.compile("(?:{}).*".format(regex))
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
if rex.match(ev[2]):
|
||||
return ev
|
||||
|
||||
def _log_event(self, evt_name, data1, data2):
|
||||
# don't show events that are anyway empty impls now
|
||||
if evt_name == "DC_EVENT_GET_STRING":
|
||||
return
|
||||
if self._debug:
|
||||
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
|
||||
self._log(evpart)
|
||||
|
||||
def _log(self, msg):
|
||||
t = threading.currentThread()
|
||||
tname = getattr(t, "name", t)
|
||||
if tname == "MainThread":
|
||||
tname = "MAIN"
|
||||
with self._loglock:
|
||||
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))
|
||||
|
||||
|
||||
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
||||
# destructor for dc_context
|
||||
dc_context_unref(dc_context)
|
||||
|
||||
@@ -58,6 +58,13 @@ class Chat(object):
|
||||
"""
|
||||
return self.id == const.DC_CHAT_ID_DEADDROP
|
||||
|
||||
def is_muted(self):
|
||||
""" return true if this chat is muted.
|
||||
|
||||
:returns: True if chat is muted, False otherwise.
|
||||
"""
|
||||
return lib.dc_chat_is_muted(self._dc_chat)
|
||||
|
||||
def is_promoted(self):
|
||||
""" return True if this chat is promoted, i.e.
|
||||
the member contacts are aware of their membership,
|
||||
@@ -84,12 +91,43 @@ class Chat(object):
|
||||
def set_name(self, name):
|
||||
""" set name of this chat.
|
||||
|
||||
:param: name as a unicode string.
|
||||
:param name: as a unicode string.
|
||||
:returns: None
|
||||
"""
|
||||
name = as_dc_charpointer(name)
|
||||
return lib.dc_set_chat_name(self._dc_context, self.id, name)
|
||||
|
||||
def mute(self, duration=None):
|
||||
""" mutes the chat
|
||||
|
||||
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
|
||||
:returns: None
|
||||
"""
|
||||
if duration is None:
|
||||
mute_duration = -1
|
||||
else:
|
||||
mute_duration = duration
|
||||
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration)
|
||||
if not bool(ret):
|
||||
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
||||
|
||||
def unmute(self):
|
||||
""" unmutes the chat
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0)
|
||||
if not bool(ret):
|
||||
raise ValueError("Failed to unmute chat")
|
||||
|
||||
def get_mute_duration(self):
|
||||
""" Returns the number of seconds until the mute of this chat is lifted.
|
||||
|
||||
: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))
|
||||
|
||||
def get_type(self):
|
||||
""" return type of this chat.
|
||||
|
||||
@@ -385,7 +423,7 @@ class Chat(object):
|
||||
"""return True if this chat is archived.
|
||||
:returns: True if archived.
|
||||
"""
|
||||
return lib.dc_chat_get_archived(self._dc_chat)
|
||||
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
|
||||
|
||||
def enable_sending_locations(self, seconds):
|
||||
"""enable sending locations for this chat.
|
||||
|
||||
@@ -18,6 +18,7 @@ DC_QR_ASK_VERIFYGROUP = 202
|
||||
DC_QR_FPR_OK = 210
|
||||
DC_QR_FPR_MISMATCH = 220
|
||||
DC_QR_FPR_WITHOUT_ADDR = 230
|
||||
DC_QR_ACCOUNT = 250
|
||||
DC_QR_ADDR = 320
|
||||
DC_QR_TEXT = 330
|
||||
DC_QR_URL = 332
|
||||
@@ -102,9 +103,15 @@ DC_EVENT_FILE_COPIED = 2055
|
||||
DC_EVENT_IS_OFFLINE = 2081
|
||||
DC_EVENT_GET_STRING = 2091
|
||||
DC_STR_SELFNOTINGRP = 21
|
||||
DC_KEY_GEN_DEFAULT = 0
|
||||
DC_KEY_GEN_RSA2048 = 1
|
||||
DC_KEY_GEN_ED25519 = 2
|
||||
DC_PROVIDER_STATUS_OK = 1
|
||||
DC_PROVIDER_STATUS_PREPARATION = 2
|
||||
DC_PROVIDER_STATUS_BROKEN = 3
|
||||
DC_CHAT_VISIBILITY_NORMAL = 0
|
||||
DC_CHAT_VISIBILITY_ARCHIVED = 1
|
||||
DC_CHAT_VISIBILITY_PINNED = 2
|
||||
DC_STR_NOMESSAGES = 1
|
||||
DC_STR_SELF = 2
|
||||
DC_STR_DRAFT = 3
|
||||
@@ -157,7 +164,7 @@ DC_STR_COUNT = 68
|
||||
|
||||
def read_event_defines(f):
|
||||
rex = re.compile(r'#define\s+((?:DC_EVENT|DC_QR|DC_MSG|DC_LP|DC_EMPTY|DC_CERTCK|DC_STATE|DC_STR|'
|
||||
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER)_\S+)\s+([x\d]+).*')
|
||||
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER|DC_KEY_GEN)_\S+)\s+([x\d]+).*')
|
||||
for line in f:
|
||||
m = rex.match(line)
|
||||
if m:
|
||||
|
||||
81
python/src/deltachat/eventlogger.py
Normal file
81
python/src/deltachat/eventlogger.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import threading
|
||||
import re
|
||||
import time
|
||||
from queue import Queue, Empty
|
||||
from .hookspec import hookimpl
|
||||
|
||||
|
||||
class EventLogger:
|
||||
_loglock = threading.RLock()
|
||||
|
||||
def __init__(self, account, logid=None, debug=True):
|
||||
self.account = account
|
||||
self._event_queue = Queue()
|
||||
self._debug = debug
|
||||
if logid is None:
|
||||
logid = str(self.account._dc_context).strip(">").split()[-1]
|
||||
self.logid = logid
|
||||
self._timeout = None
|
||||
self.init_time = time.time()
|
||||
|
||||
@hookimpl
|
||||
def process_low_level_event(self, account, event_name, data1, data2):
|
||||
if self.account == account:
|
||||
self._log_event(event_name, data1, data2)
|
||||
self._event_queue.put((event_name, data1, data2))
|
||||
|
||||
def set_timeout(self, timeout):
|
||||
self._timeout = timeout
|
||||
|
||||
def consume_events(self, check_error=True):
|
||||
while not self._event_queue.empty():
|
||||
self.get(check_error=check_error)
|
||||
|
||||
def get(self, timeout=None, check_error=True):
|
||||
timeout = timeout or self._timeout
|
||||
ev = self._event_queue.get(timeout=timeout)
|
||||
if check_error and ev[0] == "DC_EVENT_ERROR":
|
||||
raise ValueError("{}({!r},{!r})".format(*ev))
|
||||
return ev
|
||||
|
||||
def ensure_event_not_queued(self, event_name_regex):
|
||||
__tracebackhide__ = True
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
while 1:
|
||||
try:
|
||||
ev = self._event_queue.get(False)
|
||||
except Empty:
|
||||
break
|
||||
else:
|
||||
assert not rex.match(ev[0]), "event found {}".format(ev)
|
||||
|
||||
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
||||
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
while 1:
|
||||
ev = self.get(timeout=timeout, check_error=check_error)
|
||||
if rex.match(ev[0]):
|
||||
return ev
|
||||
|
||||
def get_info_matching(self, regex):
|
||||
rex = re.compile("(?:{}).*".format(regex))
|
||||
while 1:
|
||||
ev = self.get_matching("DC_EVENT_INFO")
|
||||
if rex.match(ev[2]):
|
||||
return ev
|
||||
|
||||
def _log_event(self, evt_name, data1, data2):
|
||||
# don't show events that are anyway empty impls now
|
||||
if evt_name == "DC_EVENT_GET_STRING":
|
||||
return
|
||||
if self._debug:
|
||||
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
|
||||
self._log(evpart)
|
||||
|
||||
def _log(self, msg):
|
||||
t = threading.currentThread()
|
||||
tname = getattr(t, "name", t)
|
||||
if tname == "MainThread":
|
||||
tname = "MAIN"
|
||||
with self._loglock:
|
||||
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))
|
||||
25
python/src/deltachat/hookspec.py
Normal file
25
python/src/deltachat/hookspec.py
Normal file
@@ -0,0 +1,25 @@
|
||||
""" Hooks for python bindings """
|
||||
|
||||
import pluggy
|
||||
|
||||
name = "deltachat"
|
||||
|
||||
hookspec = pluggy.HookspecMarker(name)
|
||||
hookimpl = pluggy.HookimplMarker(name)
|
||||
_plugin_manager = None
|
||||
|
||||
|
||||
def get_plugin_manager():
|
||||
global _plugin_manager
|
||||
if _plugin_manager is None:
|
||||
_plugin_manager = pluggy.PluginManager(name)
|
||||
_plugin_manager.add_hookspecs(DeltaChatHookSpecs)
|
||||
return _plugin_manager
|
||||
|
||||
|
||||
class DeltaChatHookSpecs:
|
||||
""" Plugin Hook specifications for Python bindings to Delta Chat CFFI. """
|
||||
|
||||
@hookspec
|
||||
def process_low_level_event(self, account, event_name, data1, data2):
|
||||
""" process a CFFI low level events for a given account. """
|
||||
@@ -11,27 +11,18 @@ class ProviderNotFoundError(Exception):
|
||||
class Provider(object):
|
||||
"""Provider information.
|
||||
|
||||
:param domain: The domain to get the provider info for, this is
|
||||
normally the part following the `@` of the domain.
|
||||
:param domain: The email to get the provider info for.
|
||||
"""
|
||||
|
||||
def __init__(self, domain):
|
||||
def __init__(self, account, addr):
|
||||
provider = ffi.gc(
|
||||
lib.dc_provider_new_from_domain(as_dc_charpointer(domain)),
|
||||
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
|
||||
lib.dc_provider_unref,
|
||||
)
|
||||
if provider == ffi.NULL:
|
||||
raise ProviderNotFoundError("Provider not found")
|
||||
self._provider = provider
|
||||
|
||||
@classmethod
|
||||
def from_email(cls, email):
|
||||
"""Create provider info from an email address.
|
||||
|
||||
:param email: Email address to get provider info for.
|
||||
"""
|
||||
return cls(email.split('@')[-1])
|
||||
|
||||
@property
|
||||
def overview_page(self):
|
||||
"""URL to the overview page of the provider on providers.delta.chat."""
|
||||
@@ -39,21 +30,10 @@ class Provider(object):
|
||||
lib.dc_provider_get_overview_page(self._provider))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the provider."""
|
||||
return from_dc_charpointer(lib.dc_provider_get_name(self._provider))
|
||||
|
||||
@property
|
||||
def markdown(self):
|
||||
"""Content of the information page, formatted as markdown."""
|
||||
def get_before_login_hints(self):
|
||||
"""Should be shown to the user on login."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_markdown(self._provider))
|
||||
|
||||
@property
|
||||
def status_date(self):
|
||||
"""The date the provider info was last updated, as a string."""
|
||||
return from_dc_charpointer(
|
||||
lib.dc_provider_get_status_date(self._provider))
|
||||
lib.dc_provider_get_before_login_hints(self._provider))
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import py
|
||||
import pytest
|
||||
import requests
|
||||
import time
|
||||
from deltachat import Account
|
||||
from deltachat import const
|
||||
from deltachat.capi import lib
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
import tempfile
|
||||
|
||||
|
||||
@@ -15,25 +18,41 @@ def pytest_addoption(parser):
|
||||
help="a file with >=2 lines where each line "
|
||||
"contains NAME=VALUE config settings for one account"
|
||||
)
|
||||
parser.addoption(
|
||||
"--ignored", action="store_true",
|
||||
help="Also run tests marked with the ignored marker",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
|
||||
)
|
||||
cfg = config.getoption('--liveconfig')
|
||||
if not cfg:
|
||||
cfg = os.getenv('DCC_PY_LIVECONFIG')
|
||||
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
|
||||
if cfg:
|
||||
config.option.liveconfig = cfg
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
if (list(item.iter_markers(name="ignored"))
|
||||
and not item.config.getoption("ignored")):
|
||||
pytest.skip("Ignored tests not requested, use --ignored")
|
||||
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
summary = []
|
||||
|
||||
t = tempfile.mktemp()
|
||||
m = MonkeyPatch()
|
||||
try:
|
||||
ac = Account(t, eventlogging=False)
|
||||
m.setattr(sys.stdout, "write", lambda x: len(x))
|
||||
ac = Account(t)
|
||||
info = ac.get_info()
|
||||
ac.shutdown()
|
||||
finally:
|
||||
m.undo()
|
||||
os.remove(t)
|
||||
summary.extend(['Deltachat core={} sqlite={}'.format(
|
||||
info['deltachat_core_version'],
|
||||
@@ -83,17 +102,16 @@ class SessionLiveConfigFromFile:
|
||||
|
||||
|
||||
class SessionLiveConfigFromURL:
|
||||
def __init__(self, url, create_token):
|
||||
def __init__(self, url):
|
||||
self.configlist = []
|
||||
self.url = url
|
||||
self.create_token = create_token
|
||||
|
||||
def get(self, index):
|
||||
try:
|
||||
return self.configlist[index]
|
||||
except IndexError:
|
||||
assert index == len(self.configlist), index
|
||||
res = requests.post(self.url, json={"token_create_user": int(self.create_token)})
|
||||
res = requests.post(self.url)
|
||||
if res.status_code != 200:
|
||||
pytest.skip("creating newtmpuser failed {!r}".format(res))
|
||||
d = res.json()
|
||||
@@ -110,14 +128,24 @@ def session_liveconfig(request):
|
||||
liveconfig_opt = request.config.option.liveconfig
|
||||
if liveconfig_opt:
|
||||
if liveconfig_opt.startswith("http"):
|
||||
url, create_token = liveconfig_opt.split("#", 1)
|
||||
return SessionLiveConfigFromURL(url, create_token)
|
||||
return SessionLiveConfigFromURL(liveconfig_opt)
|
||||
else:
|
||||
return SessionLiveConfigFromFile(liveconfig_opt)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def datadir():
|
||||
"""The py.path.local object of the test-data/ directory."""
|
||||
for path in reversed(py.path.local(__file__).parts()):
|
||||
datadir = path.join('test-data')
|
||||
if datadir.isdir():
|
||||
return datadir
|
||||
else:
|
||||
pytest.skip('test-data directory not found')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
|
||||
|
||||
class AccountMaker:
|
||||
def __init__(self):
|
||||
@@ -125,6 +153,8 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
|
||||
self.offline_count = 0
|
||||
self._finalizers = []
|
||||
self.init_time = time.time()
|
||||
self._generated_keys = ["alice", "bob", "charlie",
|
||||
"dom", "elena", "fiona"]
|
||||
|
||||
def finalize(self):
|
||||
while self._finalizers:
|
||||
@@ -144,26 +174,32 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
|
||||
ac._evlogger.set_timeout(2)
|
||||
return ac
|
||||
|
||||
def _preconfigure_key(self, account, addr):
|
||||
# Only set a key if we haven't used it yet for another account.
|
||||
if self._generated_keys:
|
||||
keyname = self._generated_keys.pop(0)
|
||||
fname_pub = "key/{name}-public.asc".format(name=keyname)
|
||||
fname_sec = "key/{name}-secret.asc".format(name=keyname)
|
||||
account._preconfigure_keypair(addr,
|
||||
datadir.join(fname_pub).read(),
|
||||
datadir.join(fname_sec).read())
|
||||
|
||||
def get_configured_offline_account(self):
|
||||
ac = self.get_unconfigured_account()
|
||||
|
||||
# do a pseudo-configured account
|
||||
addr = "addr{}@offline.org".format(self.offline_count)
|
||||
ac.set_config("addr", addr)
|
||||
self._preconfigure_key(ac, addr)
|
||||
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
|
||||
ac.set_config("mail_pw", "123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured", b"1")
|
||||
return ac
|
||||
|
||||
def peek_online_config(self):
|
||||
def get_online_config(self, pre_generated_key=True):
|
||||
if not session_liveconfig:
|
||||
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
|
||||
return session_liveconfig.get(self.live_count)
|
||||
|
||||
def get_online_config(self):
|
||||
if not session_liveconfig:
|
||||
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
|
||||
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
|
||||
configdict = session_liveconfig.get(self.live_count)
|
||||
self.live_count += 1
|
||||
if "e2ee_enabled" not in configdict:
|
||||
@@ -175,18 +211,24 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
|
||||
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
ac._evlogger.init_time = self.init_time
|
||||
ac._evlogger.set_timeout(30)
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False):
|
||||
ac, configdict = self.get_online_config()
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False,
|
||||
pre_generated_key=True, config={}):
|
||||
ac, configdict = self.get_online_config(
|
||||
pre_generated_key=pre_generated_key)
|
||||
configdict.update(config)
|
||||
ac.configure(**configdict)
|
||||
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
|
||||
return ac
|
||||
|
||||
def get_one_online_account(self):
|
||||
ac1 = self.get_online_configuring_account()
|
||||
def get_one_online_account(self, pre_generated_key=True):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key)
|
||||
wait_successful_IMAP_SMTP_connection(ac1)
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
return ac1
|
||||
@@ -200,10 +242,12 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
|
||||
wait_configuration_progress(ac2, 1000)
|
||||
return ac1, ac2
|
||||
|
||||
def clone_online_account(self, account):
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
self.live_count += 1
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, account.get_config("addr"))
|
||||
ac._evlogger.init_time = self.init_time
|
||||
ac._evlogger.set_timeout(30)
|
||||
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
|
||||
|
||||
@@ -6,7 +6,8 @@ import time
|
||||
from deltachat import const, Account
|
||||
from deltachat.message import Message
|
||||
from datetime import datetime, timedelta
|
||||
from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection, wait_securejoin_inviter_progress
|
||||
from conftest import (wait_configuration_progress,
|
||||
wait_securejoin_inviter_progress)
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
@@ -24,6 +25,12 @@ class TestOfflineAccountBasic:
|
||||
ac1 = Account(p.strpath, os_name="solarpunk")
|
||||
ac1.get_info()
|
||||
|
||||
def test_preconfigure_keypair(self, acfactory, datadir):
|
||||
ac = acfactory.get_unconfigured_account()
|
||||
ac._preconfigure_keypair("alice@example.com",
|
||||
datadir.join("key/alice-public.asc").read(),
|
||||
datadir.join("key/alice-secret.asc").read())
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
d = ac1.get_info()
|
||||
@@ -58,11 +65,6 @@ class TestOfflineAccountBasic:
|
||||
with pytest.raises(ValueError):
|
||||
ac1.get_self_contact()
|
||||
|
||||
def test_get_info(self, acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
out = ac1.get_infostring()
|
||||
assert "number_of_chats=0" in out
|
||||
|
||||
def test_selfcontact_configured(self, acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
me = ac1.get_self_contact()
|
||||
@@ -74,6 +76,20 @@ class TestOfflineAccountBasic:
|
||||
with pytest.raises(KeyError):
|
||||
ac1.get_config("123123")
|
||||
|
||||
def test_empty_group_bcc_self_enabled(self, acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
chat = ac1.create_group_chat(name="group1")
|
||||
msg = chat.send_text("msg1")
|
||||
assert msg in chat.get_messages()
|
||||
|
||||
def test_empty_group_bcc_self_disabled(self, acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
ac1.set_config("bcc_self", "0")
|
||||
chat = ac1.create_group_chat(name="group1")
|
||||
msg = chat.send_text("msg1")
|
||||
assert msg in chat.get_messages()
|
||||
|
||||
|
||||
class TestOfflineContact:
|
||||
def test_contact_attr(self, acfactory):
|
||||
@@ -212,6 +228,18 @@ class TestOfflineChat:
|
||||
chat.remove_profile_image()
|
||||
assert chat.get_profile_image() is None
|
||||
|
||||
def test_mute(self, ac1):
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
assert not chat.is_muted()
|
||||
chat.mute()
|
||||
assert chat.is_muted()
|
||||
chat.unmute()
|
||||
assert not chat.is_muted()
|
||||
chat.mute(50)
|
||||
assert chat.is_muted()
|
||||
with pytest.raises(ValueError):
|
||||
chat.mute(-51)
|
||||
|
||||
def test_delete_and_send_fails(self, ac1, chat1):
|
||||
chat1.delete()
|
||||
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
@@ -417,6 +445,45 @@ class TestOnlineAccount:
|
||||
ac2.create_chat_by_contact(ac2.create_contact(email=ac1.get_config("addr")))
|
||||
return chat
|
||||
|
||||
@pytest.mark.ignored
|
||||
def test_configure_generate_key(self, acfactory, lp):
|
||||
# A slow test which will generate new keys.
|
||||
ac1 = acfactory.get_online_configuring_account(
|
||||
pre_generated_key=False,
|
||||
config={"key_gen_type": str(const.DC_KEY_GEN_RSA2048)}
|
||||
)
|
||||
ac2 = acfactory.get_online_configuring_account(
|
||||
pre_generated_key=False,
|
||||
config={"key_gen_type": str(const.DC_KEY_GEN_ED25519)}
|
||||
)
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
wait_configuration_progress(ac2, 1000)
|
||||
chat = self.get_chat(ac1, ac2, both_created=True)
|
||||
|
||||
lp.sec("ac1: send unencrypted message to ac2")
|
||||
chat.send_text("message1")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
msg_in = ac2.get_message_by_id(ev[2])
|
||||
assert msg_in.text == "message1"
|
||||
assert not msg_in.is_encrypted()
|
||||
|
||||
lp.sec("ac2: send encrypted message to ac1")
|
||||
msg_in.chat.send_text("message2")
|
||||
lp.sec("ac1: waiting for message from ac2")
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
msg2_in = ac1.get_message_by_id(ev[2])
|
||||
assert msg2_in.text == "message2"
|
||||
assert msg2_in.is_encrypted()
|
||||
|
||||
lp.sec("ac1: send encrypted message to ac2")
|
||||
msg2_in.chat.send_text("message3")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
msg3_in = ac1.get_message_by_id(ev[2])
|
||||
assert msg3_in.text == "message3"
|
||||
assert msg3_in.is_encrypted()
|
||||
|
||||
def test_configure_canceled(self, acfactory):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
wait_configuration_progress(ac1, 200)
|
||||
@@ -435,37 +502,53 @@ class TestOnlineAccount:
|
||||
|
||||
def test_one_account_send_bcc_setting(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2_config = acfactory.peek_online_config()
|
||||
c2 = ac1.create_contact(email=ac2_config["addr"])
|
||||
chat = ac1.create_chat_by_contact(c2)
|
||||
assert chat.id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
wait_successful_IMAP_SMTP_connection(ac1)
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
|
||||
# Clone the first account: we will test if sent messages
|
||||
# are copied to it via BCC.
|
||||
ac1_clone = acfactory.clone_online_account(ac1)
|
||||
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
wait_configuration_progress(ac2, 1000)
|
||||
wait_configuration_progress(ac1_clone, 1000)
|
||||
|
||||
chat = self.get_chat(ac1, ac2)
|
||||
|
||||
self_addr = ac1.get_config("addr")
|
||||
other_addr = ac2.get_config("addr")
|
||||
|
||||
lp.sec("send out message without bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "0")
|
||||
msg_out = chat.send_text("message1")
|
||||
assert not msg_out.is_forwarded()
|
||||
|
||||
# wait for send out (no BCC)
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
# make sure we are not sending message to ourselves
|
||||
assert self_addr not in ev[2]
|
||||
assert other_addr in ev[2]
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
|
||||
|
||||
lp.sec("ac1: setting bcc_self=1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
msg_out = chat.send_text("message2")
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev[2] == msg_out.id
|
||||
|
||||
# wait for send out (BCC)
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
self_addr = ac1.get_config("addr")
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
# now make sure we are sending message to ourselves too
|
||||
assert self_addr in ev[2]
|
||||
assert other_addr in ev[2]
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
|
||||
|
||||
ac1._evlogger.consume_events()
|
||||
lp.sec("send out message without bcc")
|
||||
ac1.set_config("bcc_self", "0")
|
||||
msg_out = chat.send_text("message3")
|
||||
assert not msg_out.is_forwarded()
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ev[2] == msg_out.id
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
assert self_addr not in ev[2]
|
||||
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
|
||||
# Second client receives only second message, but not the first
|
||||
ev = ac1_clone._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert ac1_clone.get_message_by_id(ev[2]).text == msg_out.text
|
||||
|
||||
def test_send_file_twice_unicode_filename_mangling(self, tmpdir, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
@@ -662,7 +745,7 @@ class TestOnlineAccount:
|
||||
assert not msg_in.is_forwarded()
|
||||
assert msg_in.get_sender_contact().display_name == ac1.get_config("displayname")
|
||||
|
||||
lp.sec("check the message arrived in contact-requets/deaddrop")
|
||||
lp.sec("check the message arrived in contact-requests/deaddrop")
|
||||
chat2 = msg_in.chat
|
||||
assert msg_in in chat2.get_messages()
|
||||
assert chat2.is_deaddrop()
|
||||
@@ -1297,6 +1380,80 @@ class TestGroupStressTests:
|
||||
# Message should be encrypted because keys of other members are gossiped
|
||||
assert msg.is_encrypted()
|
||||
|
||||
def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp):
|
||||
"""
|
||||
Test that user recreates group member list when it joins the group again.
|
||||
|
||||
ac1 creates a group with two other accounts: ac2 and ac3
|
||||
Then it removes ac2, removes ac3 and adds ac2 back.
|
||||
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
|
||||
"""
|
||||
lp.sec("creating and configuring five accounts")
|
||||
accounts = [acfactory.get_online_configuring_account() for i in range(3)]
|
||||
for acc in accounts:
|
||||
wait_configuration_progress(acc, 1000)
|
||||
ac1 = accounts.pop()
|
||||
|
||||
lp.sec("ac1: setting up contacts with 2 other members")
|
||||
contacts = []
|
||||
for acc, name in zip(accounts, ["ac2", "ac3"]):
|
||||
contact = ac1.create_contact(acc.get_config("addr"), name=name)
|
||||
contacts.append(contact)
|
||||
|
||||
# make sure we accept the "hi" message
|
||||
ac1.create_chat_by_contact(contact)
|
||||
|
||||
# make sure the other side accepts our messages
|
||||
c1 = acc.create_contact(ac1.get_config("addr"), "a member")
|
||||
chat1 = acc.create_chat_by_contact(c1)
|
||||
|
||||
# send a message to get the contact key via autocrypt header
|
||||
chat1.send_text("hi")
|
||||
msg = ac1.wait_next_incoming_message()
|
||||
assert msg.text == "hi"
|
||||
|
||||
ac2, ac3 = accounts
|
||||
|
||||
lp.sec("ac1: creating group chat with 2 other members")
|
||||
chat = ac1.create_group_chat("title1")
|
||||
for contact in contacts:
|
||||
chat.add_contact(contact)
|
||||
assert not chat.is_promoted()
|
||||
|
||||
lp.sec("ac1: send mesage to new group chat")
|
||||
msg = chat.send_text("hello")
|
||||
assert chat.is_promoted()
|
||||
assert msg.is_encrypted()
|
||||
|
||||
num_contacts = len(chat.get_contacts())
|
||||
assert num_contacts == 3
|
||||
|
||||
lp.sec("checking that the chat arrived correctly")
|
||||
for ac in accounts:
|
||||
msg = ac.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
print("chat is", msg.chat)
|
||||
assert len(msg.chat.get_contacts()) == 3
|
||||
|
||||
lp.sec("ac1: removing ac2")
|
||||
chat.remove_contact(contacts[0])
|
||||
|
||||
lp.sec("ac2: wait for a message about removal from the chat")
|
||||
msg = ac2.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac1: removing ac3")
|
||||
chat.remove_contact(contacts[1])
|
||||
|
||||
lp.sec("ac1: adding ac2 back")
|
||||
# Group is promoted, message is sent automatically
|
||||
assert chat.is_promoted()
|
||||
chat.add_contact(contacts[0])
|
||||
|
||||
lp.sec("ac2: check that ac3 is removed")
|
||||
msg = ac2.wait_next_incoming_message()
|
||||
|
||||
assert len(msg.chat.get_contacts()) == len(chat.get_contacts())
|
||||
|
||||
|
||||
class TestOnlineConfigureFails:
|
||||
def test_invalid_password(self, acfactory):
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import print_function
|
||||
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
|
||||
from deltachat.capi import ffi
|
||||
from deltachat.capi import lib
|
||||
from deltachat.account import EventLogger
|
||||
|
||||
|
||||
def test_empty_context():
|
||||
@@ -18,21 +17,13 @@ def test_callback_None2int():
|
||||
|
||||
|
||||
def test_dc_close_events(tmpdir):
|
||||
ctx = ffi.gc(
|
||||
capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
evlog = EventLogger(ctx)
|
||||
evlog.set_timeout(5)
|
||||
set_context_callback(
|
||||
ctx,
|
||||
lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2)
|
||||
)
|
||||
from deltachat.account import Account
|
||||
p = tmpdir.join("hello.db")
|
||||
lib.dc_open(ctx, p.strpath.encode("ascii"), ffi.NULL)
|
||||
capi.lib.dc_close(ctx)
|
||||
ac1 = Account(p.strpath)
|
||||
ac1.shutdown()
|
||||
|
||||
def find(info_string):
|
||||
evlog = ac1._evlogger
|
||||
while 1:
|
||||
ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
|
||||
data2 = ev[2]
|
||||
@@ -102,19 +93,12 @@ def test_get_special_message_id_returns_empty_message(acfactory):
|
||||
assert msg.id == 0
|
||||
|
||||
|
||||
def test_provider_info():
|
||||
provider = lib.dc_provider_new_from_email(cutil.as_dc_charpointer("ex@example.com"))
|
||||
assert cutil.from_dc_charpointer(
|
||||
lib.dc_provider_get_overview_page(provider)
|
||||
) == "https://providers.delta.chat/example.com"
|
||||
assert cutil.from_dc_charpointer(lib.dc_provider_get_name(provider)) == "Example"
|
||||
assert cutil.from_dc_charpointer(lib.dc_provider_get_markdown(provider)) == "\n..."
|
||||
assert cutil.from_dc_charpointer(lib.dc_provider_get_status_date(provider)) == "2018-09"
|
||||
assert lib.dc_provider_get_status(provider) == const.DC_PROVIDER_STATUS_PREPARATION
|
||||
|
||||
|
||||
def test_provider_info_none():
|
||||
assert lib.dc_provider_new_from_email(cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
|
||||
|
||||
|
||||
def test_get_info_closed():
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from deltachat import const
|
||||
from deltachat import provider
|
||||
|
||||
|
||||
def test_provider_info_from_email():
|
||||
example = provider.Provider.from_email("email@example.com")
|
||||
assert example.overview_page == "https://providers.delta.chat/example.com"
|
||||
assert example.name == "Example"
|
||||
assert example.markdown == "\n..."
|
||||
assert example.status_date == "2018-09"
|
||||
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
|
||||
|
||||
|
||||
def test_provider_info_from_domain():
|
||||
example = provider.Provider("example.com")
|
||||
assert example.overview_page == "https://providers.delta.chat/example.com"
|
||||
assert example.name == "Example"
|
||||
assert example.markdown == "\n..."
|
||||
assert example.status_date == "2018-09"
|
||||
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
|
||||
|
||||
|
||||
def test_provider_info_none():
|
||||
with pytest.raises(provider.ProviderNotFoundError):
|
||||
provider.Provider.from_email("email@unexistent.no")
|
||||
@@ -7,13 +7,14 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx {posargs:tests}
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs:tests}
|
||||
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||
passenv =
|
||||
TRAVIS
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
DCC_PY_LIVECONFIG
|
||||
DCC_NEW_TMP_EMAIL
|
||||
CARGO_TARGET_DIR
|
||||
RUSTC_WRAPPER
|
||||
deps =
|
||||
@@ -65,11 +66,11 @@ commands =
|
||||
|
||||
|
||||
[pytest]
|
||||
addopts = -v -ra
|
||||
addopts = -v -ra --strict-markers
|
||||
python_files = tests/test_*.py
|
||||
norecursedirs = .tox
|
||||
xfail_strict=true
|
||||
timeout = 60
|
||||
timeout = 90
|
||||
timeout_method = thread
|
||||
|
||||
[flake8]
|
||||
|
||||
80
scripts/proxy.py
Normal file
80
scripts/proxy.py
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
# Examples:
|
||||
#
|
||||
# Original server that doesn't use SSL:
|
||||
# ./proxy.py 8080 imap.nauta.cu 143
|
||||
# ./proxy.py 8081 smtp.nauta.cu 25
|
||||
#
|
||||
# Original server that uses SSL:
|
||||
# ./proxy.py 8080 testrun.org 993 --ssl
|
||||
# ./proxy.py 8081 testrun.org 465 --ssl
|
||||
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
import selectors
|
||||
import ssl
|
||||
import socket
|
||||
import socketserver
|
||||
|
||||
|
||||
class Proxy(socketserver.ThreadingTCPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, proxy_host, proxy_port, real_host, real_port, use_ssl):
|
||||
self.real_host = real_host
|
||||
self.real_port = real_port
|
||||
self.use_ssl = use_ssl
|
||||
super().__init__((proxy_host, proxy_port), RequestHandler)
|
||||
|
||||
|
||||
class RequestHandler(socketserver.BaseRequestHandler):
|
||||
|
||||
def handle(self):
|
||||
print('{} - {} CONNECTED.'.format(datetime.now(), self.client_address))
|
||||
|
||||
total = 0
|
||||
real_server = (self.server.real_host, self.server.real_port)
|
||||
with socket.create_connection(real_server) as sock:
|
||||
if self.server.use_ssl:
|
||||
context = ssl.create_default_context()
|
||||
sock = context.wrap_socket(
|
||||
sock, server_hostname=real_server[0])
|
||||
|
||||
forward = {self.request: sock, sock: self.request}
|
||||
|
||||
sel = selectors.DefaultSelector()
|
||||
sel.register(self.request, selectors.EVENT_READ,
|
||||
self.client_address)
|
||||
sel.register(sock, selectors.EVENT_READ, real_server)
|
||||
|
||||
active = True
|
||||
while active:
|
||||
events = sel.select()
|
||||
for key, mask in events:
|
||||
print('\n{} - {} wrote:'.format(datetime.now(), key.data))
|
||||
data = key.fileobj.recv(1024)
|
||||
received = len(data)
|
||||
total += received
|
||||
print(data)
|
||||
print('{} Bytes\nTotal: {} Bytes'.format(received, total))
|
||||
if data:
|
||||
forward[key.fileobj].sendall(data)
|
||||
else:
|
||||
print('\nCLOSING CONNECTION.\n\n')
|
||||
forward[key.fileobj].close()
|
||||
key.fileobj.close()
|
||||
active = False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = argparse.ArgumentParser(description='Simple Python Proxy')
|
||||
p.add_argument(
|
||||
"proxy_port", help="the port where the proxy will listen", type=int)
|
||||
p.add_argument('host', help="the real host")
|
||||
p.add_argument('port', help="the port of the real host", type=int)
|
||||
p.add_argument("--ssl", help="use ssl to connect to the real host",
|
||||
action="store_true")
|
||||
args = p.parse_args()
|
||||
|
||||
with Proxy('', args.proxy_port, args.host, args.port, args.ssl) as proxy:
|
||||
proxy.serve_forever()
|
||||
@@ -35,7 +35,7 @@ if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
|
||||
print("{}: {}".format(x, read_toml_version(x)))
|
||||
raise SystemExit("need argument: new version, example 1.0.0-beta.27")
|
||||
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||
newversion = sys.argv[1]
|
||||
if newversion.count(".") < 2:
|
||||
raise SystemExit("need at least two dots in version")
|
||||
@@ -45,7 +45,7 @@ if __name__ == "__main__":
|
||||
assert core_toml == ffi_toml, (core_toml, ffi_toml)
|
||||
|
||||
for line in open("CHANGELOG.md"):
|
||||
## 1.0.0-beta5
|
||||
## 1.25.0
|
||||
if line.startswith("## "):
|
||||
if line[2:].strip().startswith(newversion):
|
||||
break
|
||||
|
||||
94
spec.md
94
spec.md
@@ -1,6 +1,6 @@
|
||||
# Chat-over-Email specification
|
||||
|
||||
Version 0.20.0
|
||||
Version 0.30.0
|
||||
|
||||
This document describes how emails can be used
|
||||
to implement typical messenger functions
|
||||
@@ -17,6 +17,9 @@ while staying compatible to existing MUAs.
|
||||
- [Change group name](#change-group-name)
|
||||
- [Set group image](#set-group-image)
|
||||
- [Set profile image](#set-profile-image)
|
||||
- [Locations](#locations)
|
||||
- [User locations](#user-locations)
|
||||
- [Points of interest](#points-of-interest)
|
||||
- [Miscellaneous](#miscellaneous)
|
||||
|
||||
|
||||
@@ -29,8 +32,7 @@ Messages SHOULD be encrypted by the
|
||||
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
|
||||
by the [Memoryhole](https://github.com/autocrypt/memoryhole) standard.
|
||||
If Memoryhole is not used,
|
||||
the subject of encrypted messages SHOULD be replaced by the string
|
||||
`Chat: Encrypted message` where the part after the colon MAY be localized.
|
||||
the subject of encrypted messages SHOULD be replaced by the string `...`.
|
||||
|
||||
|
||||
# Outgoing messages
|
||||
@@ -113,6 +115,7 @@ but MUAs typically expose the sender in the UI.
|
||||
Groups are chats with usually more than one recipient,
|
||||
each defined by an email-address.
|
||||
The sender plus the recipients are the group members.
|
||||
All group members form the member list.
|
||||
|
||||
To allow different groups with the same members,
|
||||
groups are identified by a group-id.
|
||||
@@ -135,8 +138,7 @@ The group-name MUST be written to `Chat-Group-Name` header
|
||||
to join a group chat on a second device any time).
|
||||
|
||||
The `Subject` header of outgoing group messages
|
||||
SHOULD start with the characters `Chat:`
|
||||
followed by the group-name and a colon followed by an excerpt of the message.
|
||||
SHOULD be set to the group-name.
|
||||
|
||||
To identify the group-id on replies from normal MUAs,
|
||||
the group-id MUST also be added to the message-id of outgoing messages.
|
||||
@@ -177,12 +179,22 @@ to a normal single-user chat with the email-address given in `From`.
|
||||
|
||||
## Add and remove members
|
||||
|
||||
Messenger clients MUST construct the member list
|
||||
from the `From`/`To` headers only on the first group message
|
||||
or if they see a `Chat-Group-Member-Added`
|
||||
or `Chat-Group-Member-Removed` action header.
|
||||
Both headers MUST have the email-address
|
||||
of the added or removed member as the value.
|
||||
Messenger clients MUST init the member list
|
||||
from the `From`/`To` headers on the first group message.
|
||||
|
||||
When a member is added later,
|
||||
a `Chat-Group-Member-Added` action header must be sent
|
||||
with the value set to the email-address of the added member.
|
||||
When receiving a `Chat-Group-Member-Added` header, however,
|
||||
_all missing_ members the `From`/`To` headers has to be added.
|
||||
This is to mitigate problems when receiving messages
|
||||
in different orders, esp. on creating new groups.
|
||||
|
||||
To remove a member, a `Chat-Group-Member-Removed` header must be sent
|
||||
with the value set to the email-address of the member to remove.
|
||||
When receiving a `Chat-Group-Member-Removed` header,
|
||||
only exaxtly the given member has to be removed from the member list.
|
||||
|
||||
Messenger clients MUST NOT construct the member list
|
||||
on other group messages
|
||||
(this is to avoid accidentally altered To-lists in normal MUAs;
|
||||
@@ -332,6 +344,64 @@ To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
|
||||
only on image changes.
|
||||
|
||||
|
||||
# Locations
|
||||
|
||||
Locations can be attachted to messages using
|
||||
[standard kml-files](https://www.opengeospatial.org/standards/kml/)
|
||||
with well-known names.
|
||||
|
||||
|
||||
## User locations
|
||||
|
||||
To send the location of the sender,
|
||||
the app can attach a file with the name `location.kml`.
|
||||
The file can contain one or more locations.
|
||||
Apps that support location streaming will typically collect some location events
|
||||
and send them together in one file.
|
||||
As each location has an independent timestamp,
|
||||
the apps can show the location as a track.
|
||||
|
||||
Note that the `addr` attribute inside the `location.kml` file
|
||||
MUST match the users email-address.
|
||||
Otherwise, the file is discarded silently;
|
||||
this is to protect against getting wrong locations,
|
||||
eg. forwarded from a normal MUA.
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document addr="ndh@deltachat.de">
|
||||
<Placemark>
|
||||
<Timestamp><when>2020-01-11T20:40:19Z</when></Timestamp>
|
||||
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
|
||||
</Placemark>
|
||||
<Placemark>
|
||||
<Timestamp><when>2020-01-11T20:40:25Z</when></Timestamp>
|
||||
<Point><coordinates accuracy="5.4">7.654,3.21</coordinates></Point>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
|
||||
|
||||
## Points of interest
|
||||
|
||||
To send an "Point of interest", a POI,
|
||||
use a normal message and attach a file with the name `message.kml`.
|
||||
In contrast to user locations, this file should contain only one location
|
||||
and an address-attribute is not needed -
|
||||
as the location belongs to the message content,
|
||||
it is fine if the location is detected on forwarding etc.
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
<Placemark>
|
||||
<Timestamp><when>2020-01-01T20:40:19Z</when></Timestamp>
|
||||
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
@@ -368,4 +438,4 @@ as the sending time of the message as indicated by its Date header,
|
||||
or the time of first receipt if that date is in the future or unavailable.
|
||||
|
||||
|
||||
Copyright © 2017-2019 Delta Chat contributors.
|
||||
Copyright © 2017-2020 Delta Chat contributors.
|
||||
|
||||
@@ -6,10 +6,10 @@ use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, str};
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::key::*;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
|
||||
/// Possible values for encryption preference
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
|
||||
@@ -52,13 +52,17 @@ impl str::FromStr for EncryptPreference {
|
||||
#[derive(Debug)]
|
||||
pub struct Aheader {
|
||||
pub addr: String,
|
||||
pub public_key: Key,
|
||||
pub public_key: SignedPublicKey,
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
}
|
||||
|
||||
impl Aheader {
|
||||
/// Creates new autocrypt header
|
||||
pub fn new(addr: String, public_key: Key, prefer_encrypt: EncryptPreference) -> Self {
|
||||
pub fn new(
|
||||
addr: String,
|
||||
public_key: SignedPublicKey,
|
||||
prefer_encrypt: EncryptPreference,
|
||||
) -> Self {
|
||||
Aheader {
|
||||
addr,
|
||||
public_key,
|
||||
@@ -71,9 +75,7 @@ impl Aheader {
|
||||
wanted_from: &str,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Option<Self> {
|
||||
use mailparse::MailHeaderMap;
|
||||
|
||||
if let Ok(Some(value)) = headers.get_first_value("Autocrypt") {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
|
||||
match Self::from_str(&value) {
|
||||
Ok(header) => {
|
||||
if addr_cmp(&header.addr, wanted_from) {
|
||||
@@ -142,22 +144,11 @@ impl str::FromStr for Aheader {
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
|
||||
let public_key = match attributes
|
||||
let public_key: SignedPublicKey = attributes
|
||||
.remove("keydata")
|
||||
.and_then(|raw| Key::from_base64(&raw, KeyType::Public))
|
||||
{
|
||||
Some(key) => {
|
||||
if key.verify() {
|
||||
key
|
||||
} else {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
.ok_or(())
|
||||
.and_then(|raw| SignedPublicKey::from_base64(&raw).or(Err(())))
|
||||
.and_then(|key| key.verify().and(Ok(key)).or(Err(())))?;
|
||||
|
||||
let prefer_encrypt = attributes
|
||||
.remove("prefer-encrypt")
|
||||
@@ -292,7 +283,7 @@ mod tests {
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::Mutual
|
||||
)
|
||||
)
|
||||
@@ -305,7 +296,7 @@ mod tests {
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::NoPreference
|
||||
)
|
||||
)
|
||||
|
||||
20
src/blob.rs
20
src/blob.rs
@@ -322,13 +322,12 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
let clean = sanitize_filename::sanitize_with_options(name, opts);
|
||||
let mut iter = clean.splitn(2, '.');
|
||||
let mut stem = iter.next().unwrap_or_default().to_string();
|
||||
let mut ext = iter.next().unwrap_or_default().to_string();
|
||||
stem.truncate(64);
|
||||
ext.truncate(32);
|
||||
match ext.len() {
|
||||
0 => (stem, "".to_string()),
|
||||
_ => (stem, format!(".{}", ext).to_lowercase()),
|
||||
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
|
||||
let ext: String = iter.next().unwrap_or_default().chars().take(32).collect();
|
||||
if ext.is_empty() {
|
||||
(stem, "".to_string())
|
||||
} else {
|
||||
(stem, format!(".{}", ext).to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,4 +630,11 @@ mod tests {
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitise_name() {
|
||||
let (_, ext) =
|
||||
BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
|
||||
assert_eq!(ext, ".txt");
|
||||
}
|
||||
}
|
||||
|
||||
1038
src/chat.rs
1038
src/chat.rs
File diff suppressed because it is too large
Load Diff
112
src/chatlist.rs
112
src/chatlist.rs
@@ -1,5 +1,6 @@
|
||||
//! # Chat list module
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::*;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
@@ -60,7 +61,7 @@ impl Chatlist {
|
||||
/// or "Not now".
|
||||
/// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
|
||||
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
|
||||
/// archived *any* chat using dc_archive_chat(). The UI should show a link as
|
||||
/// archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
|
||||
/// "Show archived chats", if the user clicks this item, the UI should show a
|
||||
/// list of all archived chats that can be created by this function hen using
|
||||
/// the DC_GCL_ARCHIVED_ONLY flag.
|
||||
@@ -73,6 +74,9 @@ impl Chatlist {
|
||||
/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
|
||||
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
|
||||
/// chats
|
||||
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||
/// and hides the device-chat,
|
||||
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
||||
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
|
||||
@@ -88,6 +92,12 @@ impl Chatlist {
|
||||
query: Option<&str>,
|
||||
query_contact_id: Option<u32>,
|
||||
) -> Result<Self> {
|
||||
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
|
||||
// messages get hidden to avoid reloading the same chatlist.
|
||||
if let Err(err) = hide_device_expired_messages(context) {
|
||||
warn!(context, "Failed to hide expired messages: {}", err);
|
||||
}
|
||||
|
||||
let mut add_archived_link_item = false;
|
||||
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
@@ -101,6 +111,14 @@ impl Chatlist {
|
||||
.map_err(Into::into)
|
||||
};
|
||||
|
||||
let skip_id = if 0 != listflags & DC_GCL_FOR_FORWARDING {
|
||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.unwrap_or_default()
|
||||
.0
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
|
||||
// select with left join and minimum:
|
||||
//
|
||||
// - the inner select must use `hidden` and _not_ `m.hidden`
|
||||
@@ -127,18 +145,21 @@ impl Chatlist {
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?))
|
||||
AND (hidden=0 OR state=?1))
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft, query_contact_id as i32],
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
)?
|
||||
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
|
||||
// show archived chats
|
||||
// (this includes the archived device-chat; we could skip it,
|
||||
// however, then the number of archived chats do not match, which might be even more irritating.
|
||||
// and adapting the number requires larger refactorings and seems not to be worth the effort)
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
@@ -178,18 +199,25 @@ impl Chatlist {
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?))
|
||||
WHERE c.id>9
|
||||
AND (hidden=0 OR state=?1))
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked=0
|
||||
AND c.name LIKE ?
|
||||
AND c.name LIKE ?3
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft, str_like_cmd],
|
||||
params![MessageState::OutDraft, skip_id, str_like_cmd],
|
||||
process_row,
|
||||
process_rows,
|
||||
)?
|
||||
} else {
|
||||
// show normal chatlist
|
||||
let sort_id_up = if 0 != listflags & DC_GCL_FOR_FORWARDING {
|
||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||
.unwrap_or_default()
|
||||
.0
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
let mut ids = context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
@@ -199,22 +227,24 @@ impl Chatlist {
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?))
|
||||
WHERE c.id>9
|
||||
AND (hidden=0 OR state=?1))
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked=0
|
||||
AND c.archived=0
|
||||
AND NOT c.archived=?3
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft],
|
||||
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
process_row,
|
||||
process_rows,
|
||||
)?;
|
||||
if 0 == listflags & DC_GCL_NO_SPECIALS {
|
||||
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) {
|
||||
ids.insert(
|
||||
0,
|
||||
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
|
||||
);
|
||||
if 0 == listflags & DC_GCL_FOR_FORWARDING {
|
||||
ids.insert(
|
||||
0,
|
||||
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
|
||||
);
|
||||
}
|
||||
}
|
||||
add_archived_link_item = true;
|
||||
}
|
||||
@@ -278,8 +308,8 @@ impl Chatlist {
|
||||
// This is because we may want to display drafts here or stuff as
|
||||
// "is typing".
|
||||
// Also, sth. as "No messages" would not work if the summary comes from a message.
|
||||
|
||||
let mut ret = Lot::new();
|
||||
|
||||
if index >= self.ids.len() {
|
||||
ret.text2 = Some("ErrBadChatlistIndex".to_string());
|
||||
return ret;
|
||||
@@ -321,6 +351,10 @@ impl Chatlist {
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
|
||||
self.ids.iter().position(|(chat_id, _)| chat_id == &id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of archived chats
|
||||
@@ -358,7 +392,6 @@ fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat;
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[test]
|
||||
@@ -378,7 +411,7 @@ mod tests {
|
||||
// drafts are sorted to the top
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
set_draft(&t.ctx, chat_id2, Some(&mut msg));
|
||||
chat_id2.set_draft(&t.ctx, Some(&mut msg));
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.get_chat_id(0), chat_id2);
|
||||
|
||||
@@ -389,11 +422,32 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
chat::archive(&t.ctx, chat_id1, true).ok();
|
||||
chat_id1
|
||||
.set_visibility(&t.ctx, ChatVisibility::Archived)
|
||||
.ok();
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_self_talk_up_on_forward() {
|
||||
let t = dummy_context();
|
||||
t.ctx.update_device_chats().unwrap();
|
||||
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert!(chats.len() == 3);
|
||||
assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None).unwrap();
|
||||
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
|
||||
assert!(Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_special_chat_names() {
|
||||
let t = dummy_context();
|
||||
@@ -416,4 +470,18 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_summary_unwrap() {
|
||||
let t = dummy_context();
|
||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
|
||||
chat_id1.set_draft(&t.ctx, Some(&mut msg));
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
let summary = chats.get_summary(&t.ctx, 0, None);
|
||||
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@ use strum::{EnumProperty, IntoEnumIterator};
|
||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::ChatId;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::events::Event;
|
||||
use crate::job::*;
|
||||
use crate::message::MsgId;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::stock::StockMessage;
|
||||
use rusqlite::NO_PARAMS;
|
||||
|
||||
@@ -61,6 +65,28 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
|
||||
ShowEmails,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
KeyGenType,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// server.
|
||||
///
|
||||
/// Equals to 0 by default, which means the message is never
|
||||
/// deleted.
|
||||
///
|
||||
/// Value 1 is treated as "delete at once": messages are deleted
|
||||
/// immediately, without moving to DeltaChat folder.
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteServerAfter,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// device.
|
||||
///
|
||||
/// Equals to 0 by default, which means the message is never
|
||||
/// deleted.
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteDeviceAfter,
|
||||
|
||||
SaveMimeHeaders,
|
||||
ConfiguredAddr,
|
||||
ConfiguredMailServer,
|
||||
@@ -98,7 +124,7 @@ impl Context {
|
||||
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
|
||||
}
|
||||
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{}", 24 * 1024 * 1024 / 4 * 3)),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
|
||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||
_ => self.sql.get_raw_config(self, key),
|
||||
};
|
||||
@@ -124,6 +150,29 @@ impl Context {
|
||||
self.get_config_int(key) != 0
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
/// at once, `Some(x)` means delete after `x` seconds.
|
||||
pub fn get_config_delete_server_after(&self) -> Option<i64> {
|
||||
match self.get_config_int(Config::DeleteServerAfter) {
|
||||
0 => None,
|
||||
1 => Some(0),
|
||||
x => Some(x as i64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets configured "delete_device_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(x)` means delete
|
||||
/// after `x` seconds.
|
||||
pub fn get_config_delete_device_after(&self) -> Option<i64> {
|
||||
match self.get_config_int(Config::DeleteDeviceAfter) {
|
||||
0 => None,
|
||||
x => Some(x as i64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the given config key.
|
||||
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
|
||||
pub fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
|
||||
@@ -167,6 +216,15 @@ impl Context {
|
||||
|
||||
self.sql.set_raw_config(self, key, val)
|
||||
}
|
||||
Config::DeleteDeviceAfter => {
|
||||
let ret = self.sql.set_raw_config(self, key, value);
|
||||
// Force chatlist reload to delete old messages immediately.
|
||||
self.call_cb(Event::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
});
|
||||
ret
|
||||
}
|
||||
_ => self.sql.set_raw_config(self, key, value),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,13 @@ use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee;
|
||||
use crate::job::{self, job_add, job_kill_action};
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::oauth2::*;
|
||||
use crate::param::Params;
|
||||
use crate::{chat, e2ee, provider};
|
||||
|
||||
use crate::message::Message;
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
|
||||
@@ -31,26 +32,28 @@ macro_rules! progress {
|
||||
};
|
||||
}
|
||||
|
||||
// connect
|
||||
pub fn configure(context: &Context) {
|
||||
if context.has_ongoing() {
|
||||
warn!(context, "There is already another ongoing process running.",);
|
||||
return;
|
||||
impl Context {
|
||||
/// Starts a configuration job.
|
||||
pub fn configure(&self) {
|
||||
if self.has_ongoing() {
|
||||
warn!(self, "There is already another ongoing process running.",);
|
||||
return;
|
||||
}
|
||||
job_kill_action(self, job::Action::ConfigureImap);
|
||||
job_add(self, job::Action::ConfigureImap, 0, Params::new(), 0);
|
||||
}
|
||||
job_kill_action(context, job::Action::ConfigureImap);
|
||||
job_add(context, job::Action::ConfigureImap, 0, Params::new(), 0);
|
||||
}
|
||||
|
||||
/// Check if the context is already configured.
|
||||
pub fn dc_is_configured(context: &Context) -> bool {
|
||||
context.sql.get_raw_config_bool(context, "configured")
|
||||
/// Checks if the context is already configured.
|
||||
pub fn is_configured(&self) -> bool {
|
||||
self.sql.get_raw_config_bool(self, "configured")
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* Configure JOB
|
||||
******************************************************************************/
|
||||
#[allow(non_snake_case, unused_must_use, clippy::cognitive_complexity)]
|
||||
pub fn JobConfigureImap(context: &Context) -> job::Status {
|
||||
pub(crate) fn JobConfigureImap(context: &Context) -> job::Status {
|
||||
if !context.sql.is_open() {
|
||||
error!(context, "Cannot configure, database not opened.",);
|
||||
progress!(context, 0);
|
||||
@@ -200,10 +203,7 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
|
||||
7 => {
|
||||
progress!(context, 310);
|
||||
if param_autoconfig.is_none() {
|
||||
let url = format!(
|
||||
"https://{}{}/autodiscover/autodiscover.xml",
|
||||
"", param_domain
|
||||
);
|
||||
let url = format!("https://{}/autodiscover/autodiscover.xml", param_domain);
|
||||
param_autoconfig = outlk_autodiscover(context, &url, ¶m).ok();
|
||||
}
|
||||
true
|
||||
@@ -371,7 +371,7 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
|
||||
let create_mvbox = context.get_config_bool(Config::MvboxWatch)
|
||||
|| context.get_config_bool(Config::MvboxMove);
|
||||
let imap = &context.inbox_thread.read().unwrap().imap;
|
||||
if let Err(err) = imap.ensure_configured_folders(context, create_mvbox) {
|
||||
if let Err(err) = imap.configure_folders(context, create_mvbox) {
|
||||
warn!(context, "configuring folders failed: {:?}", err);
|
||||
false
|
||||
} else {
|
||||
@@ -439,45 +439,77 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
|
||||
LoginParam::from_database(context, "configured_raw_").save_to_database(context, "");
|
||||
}
|
||||
|
||||
if let Some(provider) = provider::get_provider_info(¶m.addr) {
|
||||
if !provider.after_login_hint.is_empty() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(provider.after_login_hint.to_string());
|
||||
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg)).is_err() {
|
||||
warn!(context, "cannot add after_login_hint as core-provider-info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.free_ongoing();
|
||||
progress!(context, if success { 1000 } else { 0 });
|
||||
job::Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_unwrap)]
|
||||
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
|
||||
// XXX we don't have https://github.com/deltachat/provider-db APIs
|
||||
// integrated yet but we'll already add nauta as a first use case, also
|
||||
// showing what we need from provider-db in the future.
|
||||
info!(
|
||||
context,
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
if param.addr.ends_with("@nauta.cu") {
|
||||
let mut p = LoginParam::new();
|
||||
if let Some(provider) = provider::get_provider_info(¶m.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 imap.is_some() && smtp.is_some() {
|
||||
let imap = imap.unwrap();
|
||||
let smtp = smtp.unwrap();
|
||||
|
||||
p.addr = param.addr.clone();
|
||||
p.mail_server = "imap.nauta.cu".to_string();
|
||||
p.mail_user = param.addr.clone();
|
||||
p.mail_pw = param.mail_pw.clone();
|
||||
p.mail_port = 143;
|
||||
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
|
||||
let mut p = LoginParam::new();
|
||||
p.addr = param.addr.clone();
|
||||
|
||||
p.send_server = "smtp.nauta.cu".to_string();
|
||||
p.send_user = param.addr.clone();
|
||||
p.send_pw = param.mail_pw.clone();
|
||||
p.send_port = 25;
|
||||
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
|
||||
p.server_flags = DC_LP_AUTH_NORMAL as i32
|
||||
| DC_LP_IMAP_SOCKET_STARTTLS as i32
|
||||
| DC_LP_SMTP_SOCKET_STARTTLS as i32;
|
||||
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,
|
||||
};
|
||||
|
||||
info!(context, "found offline autoconfig: {}", p);
|
||||
Some(p)
|
||||
} else {
|
||||
info!(context, "no offline autoconfig found");
|
||||
None
|
||||
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);
|
||||
} else {
|
||||
info!(context, "offline autoconfig found, but no servers defined");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
provider::Status::BROKEN => {
|
||||
info!(context, "offline autoconfig found, provider is broken");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!(context, "no offline autoconfig found");
|
||||
None
|
||||
}
|
||||
|
||||
fn try_imap_connections(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use deltachat_derive::*;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
|
||||
@@ -15,7 +16,20 @@ const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
|
||||
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
|
||||
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum Blocked {
|
||||
Not = 0,
|
||||
@@ -43,7 +57,19 @@ impl Default for ShowEmails {
|
||||
}
|
||||
}
|
||||
|
||||
pub const DC_IMAP_SEEN: u32 = 0x1;
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(u8)]
|
||||
pub enum KeyGenType {
|
||||
Default = 0,
|
||||
Rsa2048 = 1,
|
||||
Ed25519 = 2,
|
||||
}
|
||||
|
||||
impl Default for KeyGenType {
|
||||
fn default() -> Self {
|
||||
KeyGenType::Default
|
||||
}
|
||||
}
|
||||
|
||||
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
|
||||
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
|
||||
@@ -54,6 +80,7 @@ pub(crate) const DC_FROM_HANDSHAKE: i32 = 0x01;
|
||||
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
|
||||
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
|
||||
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
|
||||
pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
|
||||
|
||||
pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
|
||||
|
||||
@@ -90,6 +117,8 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
|
||||
FromSql,
|
||||
ToSql,
|
||||
IntoStaticStr,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Chattype {
|
||||
@@ -185,7 +214,23 @@ pub const DC_BOB_SUCCESS: i32 = 1;
|
||||
// max. width/height of an avatar
|
||||
pub const AVATAR_SIZE: u32 = 192;
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
// this value can be increased if the folder configuration is changed and must be redone on next program start
|
||||
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
pub enum Viewtype {
|
||||
Unknown = 0,
|
||||
|
||||
166
src/contact.rs
166
src/contact.rs
@@ -60,7 +60,7 @@ pub struct Contact {
|
||||
/// to access this field.
|
||||
authname: String,
|
||||
|
||||
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr`` to access this field.
|
||||
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
|
||||
addr: String,
|
||||
|
||||
/// Blocked state. Use dc_contact_is_blocked to access this field.
|
||||
@@ -118,7 +118,7 @@ pub enum Origin {
|
||||
Internal = 0x40000,
|
||||
|
||||
/// address is in our address book
|
||||
AdressBook = 0x80000,
|
||||
AddressBook = 0x80000,
|
||||
|
||||
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
|
||||
SecurejoinInvited = 0x0100_0000,
|
||||
@@ -146,7 +146,7 @@ impl Origin {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Modifier {
|
||||
pub(crate) enum Modifier {
|
||||
None,
|
||||
Modified,
|
||||
Created,
|
||||
@@ -300,9 +300,31 @@ impl Contact {
|
||||
}
|
||||
|
||||
/// Lookup a contact and create it if it does not exist yet.
|
||||
/// The contact is identified by the email-address, a name and an "origin" can be given.
|
||||
///
|
||||
/// The "origin" is where the address comes from -
|
||||
/// from-header, cc-header, addressbook, qr, manual-edit etc.
|
||||
/// In general, "better" origins overwrite the names of "worse" origins -
|
||||
/// Eg. if we got a name in cc-header and later in from-header, the name will change -
|
||||
/// this does not happen the other way round.
|
||||
///
|
||||
/// The "best" origin are manually created contacts -
|
||||
/// names given manually can only be overwritten by further manual edits
|
||||
/// (until they are set empty again or reset to the name seen in the From-header).
|
||||
///
|
||||
/// These manually edited names are _never_ used for sending on the wire -
|
||||
/// this should avoid sending sth. as "Mama" or "Daddy" to some 3rd party.
|
||||
/// Instead, for the wire, we use so called "authnames"
|
||||
/// that can only be set and updated by a From-header.
|
||||
///
|
||||
/// The different names used in the function are:
|
||||
/// - "name": name passed as function argument, belonging to the given origin
|
||||
/// - "row_name": current name used in the database, typically set to "name"
|
||||
/// - "row_authname": name as authorized from a contact, set only through a From-header
|
||||
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
|
||||
///
|
||||
/// Returns the contact_id and a `Modifier` value indicating if a modification occured.
|
||||
pub fn add_or_lookup(
|
||||
pub(crate) fn add_or_lookup(
|
||||
context: &Context,
|
||||
name: impl AsRef<str>,
|
||||
addr: impl AsRef<str>,
|
||||
@@ -356,7 +378,9 @@ impl Contact {
|
||||
|
||||
if !name.as_ref().is_empty() {
|
||||
if !row_name.is_empty() {
|
||||
if origin >= row_origin && name.as_ref() != row_name {
|
||||
if (origin >= row_origin || row_name == row_authname)
|
||||
&& name.as_ref() != row_name
|
||||
{
|
||||
update_name = true;
|
||||
}
|
||||
} else {
|
||||
@@ -365,6 +389,9 @@ impl Contact {
|
||||
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
|
||||
update_authname = true;
|
||||
}
|
||||
} else if origin == Origin::ManuallyCreated && !row_authname.is_empty() {
|
||||
// no name given on manual edit, this will update the name to the authname
|
||||
update_name = true;
|
||||
}
|
||||
|
||||
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
||||
@@ -375,16 +402,22 @@ impl Contact {
|
||||
update_addr = true;
|
||||
}
|
||||
if update_name || update_authname || update_addr || origin > row_origin {
|
||||
let new_name = if update_name {
|
||||
if !name.as_ref().is_empty() {
|
||||
name.as_ref()
|
||||
} else {
|
||||
&row_authname
|
||||
}
|
||||
} else {
|
||||
&row_name
|
||||
};
|
||||
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
|
||||
params![
|
||||
if update_name {
|
||||
name.as_ref()
|
||||
} else {
|
||||
&row_name
|
||||
},
|
||||
new_name,
|
||||
if update_addr { addr } else { &row_addr },
|
||||
if origin > row_origin {
|
||||
origin
|
||||
@@ -402,11 +435,13 @@ impl Contact {
|
||||
.ok();
|
||||
|
||||
if update_name {
|
||||
// Update the contact name also if it is used as a group name.
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
|
||||
params![name.as_ref(), Chattype::Single, row_id]
|
||||
params![new_name, Chattype::Single, row_id]
|
||||
).ok();
|
||||
}
|
||||
sth_modified = Modifier::Modified;
|
||||
@@ -462,9 +497,18 @@ impl Contact {
|
||||
|
||||
for (name, addr) in split_address_book(addr_book.as_ref()).into_iter() {
|
||||
let name = normalize_name(name);
|
||||
let (_, modified) = Contact::add_or_lookup(context, name, addr, Origin::AdressBook)?;
|
||||
if modified != Modifier::None {
|
||||
modify_cnt += 1
|
||||
match Contact::add_or_lookup(context, name, addr, Origin::AddressBook) {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to add address {} from address book: {}", addr, err
|
||||
);
|
||||
}
|
||||
Ok((_, modified)) => {
|
||||
if modified != Modifier::None {
|
||||
modify_cnt += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if modify_cnt > 0 {
|
||||
@@ -984,7 +1028,7 @@ fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_profile_image(
|
||||
pub(crate) fn set_profile_image(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
profile_image: &AvatarAction,
|
||||
@@ -1001,7 +1045,6 @@ pub fn set_profile_image(
|
||||
contact.param.remove(Param::ProfileImage);
|
||||
true
|
||||
}
|
||||
AvatarAction::None => false,
|
||||
};
|
||||
if changed {
|
||||
contact.update_param(context)?;
|
||||
@@ -1024,7 +1067,7 @@ pub fn normalize_name(full_name: impl AsRef<str>) -> String {
|
||||
}
|
||||
|
||||
let len = full_name.len();
|
||||
if len > 0 {
|
||||
if len > 1 {
|
||||
let firstchar = full_name.as_bytes()[0];
|
||||
let lastchar = full_name.as_bytes()[len - 1];
|
||||
if firstchar == b'\'' && lastchar == b'\''
|
||||
@@ -1133,6 +1176,10 @@ mod tests {
|
||||
fn test_normalize_name() {
|
||||
assert_eq!(&normalize_name("Doe, John"), "John Doe");
|
||||
assert_eq!(&normalize_name(" hello world "), "hello world");
|
||||
assert_eq!(&normalize_name("<"), "<");
|
||||
assert_eq!(&normalize_name(">"), ">");
|
||||
assert_eq!(&normalize_name("'"), "'");
|
||||
assert_eq!(&normalize_name("\""), "\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1195,6 +1242,7 @@ mod tests {
|
||||
let book = concat!(
|
||||
" Name one \n one@eins.org \n",
|
||||
"Name two\ntwo@deux.net\n",
|
||||
"Invalid\n+1234567890\n", // invalid, should be ignored
|
||||
"\nthree@drei.sam\n",
|
||||
"Name two\ntwo@deux.net\n" // should not be added again
|
||||
);
|
||||
@@ -1281,6 +1329,7 @@ mod tests {
|
||||
fn test_remote_authnames() {
|
||||
let t = dummy_context();
|
||||
|
||||
// incoming mail `From: bob1 <bob@example.org>` - this should init authname and name
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t.ctx,
|
||||
"bob1",
|
||||
@@ -1295,6 +1344,7 @@ mod tests {
|
||||
assert_eq!(contact.get_name(), "bob1");
|
||||
assert_eq!(contact.get_display_name(), "bob1");
|
||||
|
||||
// incoming mail `From: bob2 <bob@example.org>` - this should update authname and name
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t.ctx,
|
||||
"bob2",
|
||||
@@ -1309,16 +1359,15 @@ mod tests {
|
||||
assert_eq!(contact.get_name(), "bob2");
|
||||
assert_eq!(contact.get_display_name(), "bob2");
|
||||
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(&t.ctx, "bob3", "bob@example.org", Origin::ManuallyCreated)
|
||||
.unwrap();
|
||||
// manually edit name to "bob3" - authname should be still be "bob2" a given in `From:` above
|
||||
let contact_id = Contact::create(&t.ctx, "bob3", "bob@example.org").unwrap();
|
||||
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
||||
assert_eq!(contact.get_authname(), "bob2");
|
||||
assert_eq!(contact.get_name(), "bob3");
|
||||
assert_eq!(contact.get_display_name(), "bob3");
|
||||
|
||||
// incoming mail `From: bob4 <bob@example.org>` - this should update authname, manually given name is still "bob3"
|
||||
let (contact_id, sth_modified) = Contact::add_or_lookup(
|
||||
&t.ctx,
|
||||
"bob4",
|
||||
@@ -1334,6 +1383,81 @@ mod tests {
|
||||
assert_eq!(contact.get_display_name(), "bob3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remote_authnames_create_empty() {
|
||||
let t = dummy_context();
|
||||
|
||||
// manually create "claire@example.org" without a given name
|
||||
let contact_id = Contact::create(&t.ctx, "", "claire@example.org").unwrap();
|
||||
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
|
||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "claire@example.org");
|
||||
|
||||
// incoming mail `From: claire1 <claire@example.org>` - this should update authname and name
|
||||
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
|
||||
&t.ctx,
|
||||
"claire1",
|
||||
"claire@example.org",
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(contact_id, contact_id_same);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
||||
assert_eq!(contact.get_authname(), "claire1");
|
||||
assert_eq!(contact.get_name(), "claire1");
|
||||
assert_eq!(contact.get_display_name(), "claire1");
|
||||
|
||||
// incoming mail `From: claire2 <claire@example.org>` - this should update authname and name
|
||||
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
|
||||
&t.ctx,
|
||||
"claire2",
|
||||
"claire@example.org",
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(contact_id, contact_id_same);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
||||
assert_eq!(contact.get_authname(), "claire2");
|
||||
assert_eq!(contact.get_name(), "claire2");
|
||||
assert_eq!(contact.get_display_name(), "claire2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remote_authnames_edit_empty() {
|
||||
let t = dummy_context();
|
||||
|
||||
// manually create "dave@example.org"
|
||||
let contact_id = Contact::create(&t.ctx, "dave1", "dave@example.org").unwrap();
|
||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_name(), "dave1");
|
||||
assert_eq!(contact.get_display_name(), "dave1");
|
||||
|
||||
// incoming mail `From: dave2 <dave@example.org>` - this should update authname
|
||||
Contact::add_or_lookup(
|
||||
&t.ctx,
|
||||
"dave2",
|
||||
"dave@example.org",
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.unwrap();
|
||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
||||
assert_eq!(contact.get_authname(), "dave2");
|
||||
assert_eq!(contact.get_name(), "dave1");
|
||||
assert_eq!(contact.get_display_name(), "dave1");
|
||||
|
||||
// manually clear the name
|
||||
Contact::create(&t.ctx, "", "dave@example.org").unwrap();
|
||||
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
|
||||
assert_eq!(contact.get_authname(), "dave2");
|
||||
assert_eq!(contact.get_name(), "dave2");
|
||||
assert_eq!(contact.get_display_name(), "dave2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_addr_cmp() {
|
||||
assert!(addr_cmp("AA@AA.ORG", "aa@aa.ORG"));
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::events::Event;
|
||||
use crate::imap::*;
|
||||
use crate::job::*;
|
||||
use crate::job_thread::JobThread;
|
||||
use crate::key::*;
|
||||
use crate::key::Key;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{self, Message, MessengerMessage, MsgId};
|
||||
@@ -51,7 +51,7 @@ pub struct Context {
|
||||
cb: Box<ContextCallback>,
|
||||
pub os_name: Option<String>,
|
||||
pub cmdline_sel_chat_id: Arc<RwLock<ChatId>>,
|
||||
pub bob: Arc<RwLock<BobStatus>>,
|
||||
pub(crate) bob: Arc<RwLock<BobStatus>>,
|
||||
pub last_smeared_timestamp: RwLock<i64>,
|
||||
pub running_state: Arc<RwLock<RunningState>>,
|
||||
/// Mutex to avoid generating the key for the user more than once.
|
||||
@@ -83,6 +83,8 @@ pub fn get_info() -> HashMap<&'static str, String> {
|
||||
impl Context {
|
||||
/// Creates new context.
|
||||
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> {
|
||||
pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
let mut blob_fname = OsString::new();
|
||||
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
||||
blob_fname.push("-blobs");
|
||||
@@ -476,14 +478,14 @@ impl Default for RunningState {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BobStatus {
|
||||
pub(crate) struct BobStatus {
|
||||
pub expects: i32,
|
||||
pub status: i32,
|
||||
pub qr_scan: Option<Lot>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum PerformJobsNeeded {
|
||||
pub(crate) enum PerformJobsNeeded {
|
||||
Not,
|
||||
AtOnce,
|
||||
AvoidDos,
|
||||
@@ -500,7 +502,7 @@ pub struct SmtpState {
|
||||
pub idle: bool,
|
||||
pub suspended: bool,
|
||||
pub doing_jobs: bool,
|
||||
pub perform_jobs_needed: PerformJobsNeeded,
|
||||
pub(crate) perform_jobs_needed: PerformJobsNeeded,
|
||||
pub probe_network: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::message::{self, MessageState, MessengerMessage, MsgId};
|
||||
use crate::mimeparser::*;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::securejoin::{self, handle_securejoin_handshake};
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::{contact, location};
|
||||
@@ -37,7 +37,7 @@ pub fn dc_receive_imf(
|
||||
imf_raw: &[u8],
|
||||
server_folder: impl AsRef<str>,
|
||||
server_uid: u32,
|
||||
flags: u32,
|
||||
seen: bool,
|
||||
) -> Result<()> {
|
||||
info!(
|
||||
context,
|
||||
@@ -74,18 +74,12 @@ pub fn dc_receive_imf(
|
||||
// helper method to handle early exit and memory cleanup
|
||||
let cleanup = |context: &Context,
|
||||
create_event_to_send: &Option<CreateEvent>,
|
||||
created_db_entries: &Vec<(ChatId, MsgId)>| {
|
||||
created_db_entries: Vec<(ChatId, MsgId)>| {
|
||||
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: *msg_id,
|
||||
chat_id: *chat_id,
|
||||
},
|
||||
CreateEvent::IncomingMsg => Event::IncomingMsg {
|
||||
msg_id: *msg_id,
|
||||
chat_id: *chat_id,
|
||||
},
|
||||
CreateEvent::MsgsChanged => Event::MsgsChanged { msg_id, chat_id },
|
||||
CreateEvent::IncomingMsg => Event::IncomingMsg { msg_id, chat_id },
|
||||
};
|
||||
context.call_cb(event);
|
||||
}
|
||||
@@ -100,41 +94,15 @@ pub fn dc_receive_imf(
|
||||
// get From: (it can be an address list!) and check if it is known (for known From:'s we add
|
||||
// the other To:/Cc: in the 3rd pass)
|
||||
// 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 issue #150)
|
||||
let mut from_id = 0;
|
||||
let mut from_id_blocked = false;
|
||||
let mut incoming = true;
|
||||
let mut incoming_origin = Origin::Unknown;
|
||||
|
||||
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
|
||||
let from_ids = dc_add_or_lookup_contacts_by_address_list(
|
||||
context,
|
||||
&field_from,
|
||||
Origin::IncomingUnknownFrom,
|
||||
)?;
|
||||
if from_ids.contains(&DC_CONTACT_ID_SELF) {
|
||||
incoming = false;
|
||||
from_id = DC_CONTACT_ID_SELF;
|
||||
incoming_origin = Origin::OutgoingBcc;
|
||||
} else if !from_ids.is_empty() {
|
||||
if from_ids.len() > 1 {
|
||||
warn!(
|
||||
context,
|
||||
"mail has more than one From address, only using first: {:?}", field_from
|
||||
);
|
||||
}
|
||||
from_id = from_ids.get_index(0).cloned().unwrap_or_default();
|
||||
if let Ok(contact) = Contact::load_from_db(context, from_id) {
|
||||
incoming_origin = contact.origin;
|
||||
from_id_blocked = contact.blocked;
|
||||
}
|
||||
// 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) =
|
||||
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
|
||||
from_field_to_contact_id(context, field_from)?
|
||||
} else {
|
||||
warn!(context, "mail has an empty From header: {:?}", field_from);
|
||||
// if there is no from given, from_id stays 0 which is just fine. These messages
|
||||
// are very rare, however, we have to add them to the database (they go to the
|
||||
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
|
||||
}
|
||||
}
|
||||
(0, false, Origin::Unknown)
|
||||
};
|
||||
let incoming = from_id != DC_CONTACT_ID_SELF;
|
||||
|
||||
let mut to_ids = ContactIds::new();
|
||||
for header_def in &[HeaderDef::To, HeaderDef::Cc] {
|
||||
@@ -185,13 +153,13 @@ pub fn dc_receive_imf(
|
||||
from_id_blocked,
|
||||
&mut hidden,
|
||||
&mut chat_id,
|
||||
flags,
|
||||
seen,
|
||||
&mut needs_delete_job,
|
||||
&mut insert_msg_id,
|
||||
&mut created_db_entries,
|
||||
&mut create_event_to_send,
|
||||
) {
|
||||
cleanup(context, &create_event_to_send, &created_db_entries);
|
||||
cleanup(context, &create_event_to_send, created_db_entries);
|
||||
bail!("add_parts error: {:?}", err);
|
||||
}
|
||||
} else {
|
||||
@@ -213,8 +181,8 @@ pub fn dc_receive_imf(
|
||||
);
|
||||
}
|
||||
|
||||
if mime_parser.user_avatar != AvatarAction::None {
|
||||
match contact::set_profile_image(&context, from_id, &mime_parser.user_avatar) {
|
||||
if let Some(avatar_action) = &mime_parser.user_avatar {
|
||||
match contact::set_profile_image(&context, from_id, avatar_action) {
|
||||
Ok(()) => {
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
}
|
||||
@@ -224,17 +192,24 @@ pub fn dc_receive_imf(
|
||||
};
|
||||
}
|
||||
|
||||
// if we delete we don't need to try moving messages
|
||||
if needs_delete_job && !created_db_entries.is_empty() {
|
||||
job_add(
|
||||
context,
|
||||
Action::DeleteMsgOnImap,
|
||||
created_db_entries[0].1.to_u32() as i32,
|
||||
Params::new(),
|
||||
0,
|
||||
);
|
||||
} else {
|
||||
context.do_heuristics_moves(server_folder.as_ref(), insert_msg_id);
|
||||
// Get user-configured server deletion
|
||||
let delete_server_after = context.get_config_delete_server_after();
|
||||
|
||||
if !created_db_entries.is_empty() {
|
||||
if needs_delete_job || delete_server_after == Some(0) {
|
||||
for db_entry in &created_db_entries {
|
||||
job_add(
|
||||
context,
|
||||
Action::DeleteMsgOnImap,
|
||||
db_entry.1.to_u32() as i32,
|
||||
Params::new(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Move message if we don't delete it immediately.
|
||||
context.do_heuristics_moves(server_folder.as_ref(), insert_msg_id);
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
@@ -242,13 +217,54 @@ pub fn dc_receive_imf(
|
||||
"received message {} has Message-Id: {}", server_uid, rfc724_mid
|
||||
);
|
||||
|
||||
cleanup(context, &create_event_to_send, &created_db_entries);
|
||||
cleanup(context, &create_event_to_send, created_db_entries);
|
||||
|
||||
mime_parser.handle_reports(from_id, sent_timestamp, &server_folder, server_uid);
|
||||
mime_parser.handle_reports(context, from_id, sent_timestamp);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts "From" field to contact id.
|
||||
///
|
||||
/// Also returns whether it is blocked or not and its origin.
|
||||
pub fn from_field_to_contact_id(
|
||||
context: &Context,
|
||||
field_from: &str,
|
||||
) -> Result<(u32, bool, Origin)> {
|
||||
let from_ids = dc_add_or_lookup_contacts_by_address_list(
|
||||
context,
|
||||
&field_from,
|
||||
Origin::IncomingUnknownFrom,
|
||||
)?;
|
||||
|
||||
if from_ids.contains(&DC_CONTACT_ID_SELF) {
|
||||
Ok((DC_CONTACT_ID_SELF, false, Origin::OutgoingBcc))
|
||||
} else if !from_ids.is_empty() {
|
||||
if from_ids.len() > 1 {
|
||||
warn!(
|
||||
context,
|
||||
"mail has more than one From address, only using first: {:?}", field_from
|
||||
);
|
||||
}
|
||||
let from_id = from_ids.get_index(0).cloned().unwrap_or_default();
|
||||
|
||||
let mut from_id_blocked = false;
|
||||
let mut incoming_origin = Origin::Unknown;
|
||||
if let Ok(contact) = Contact::load_from_db(context, from_id) {
|
||||
from_id_blocked = contact.blocked;
|
||||
incoming_origin = contact.origin;
|
||||
}
|
||||
Ok((from_id, from_id_blocked, incoming_origin))
|
||||
} else {
|
||||
warn!(context, "mail has an empty From header: {:?}", field_from);
|
||||
// if there is no from given, from_id stays 0 which is just fine. These messages
|
||||
// are very rare, however, we have to add them to the database (they go to the
|
||||
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
|
||||
|
||||
Ok((0, false, Origin::Unknown))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
|
||||
fn add_parts(
|
||||
context: &Context,
|
||||
@@ -265,7 +281,7 @@ fn add_parts(
|
||||
from_id_blocked: bool,
|
||||
hidden: &mut bool,
|
||||
chat_id: &mut ChatId,
|
||||
flags: u32,
|
||||
seen: bool,
|
||||
needs_delete_job: &mut bool,
|
||||
insert_msg_id: &mut MsgId,
|
||||
created_db_entries: &mut Vec<(ChatId, MsgId)>,
|
||||
@@ -299,8 +315,7 @@ fn add_parts(
|
||||
} else {
|
||||
MessengerMessage::No
|
||||
};
|
||||
// incoming non-chat messages may be discarded;
|
||||
// maybe this can be optimized later, by checking the state before the message body is downloaded
|
||||
// incoming non-chat messages may be discarded
|
||||
let mut allow_creation = true;
|
||||
let show_emails =
|
||||
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default();
|
||||
@@ -308,11 +323,13 @@ fn add_parts(
|
||||
&& msgrmsg == MessengerMessage::No
|
||||
{
|
||||
// this message is a classic email not a chat-message nor a reply to one
|
||||
if show_emails == ShowEmails::Off {
|
||||
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
|
||||
allow_creation = false
|
||||
} else if show_emails == ShowEmails::AcceptedContacts {
|
||||
allow_creation = false
|
||||
match show_emails {
|
||||
ShowEmails::Off => {
|
||||
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
|
||||
allow_creation = false;
|
||||
}
|
||||
ShowEmails::AcceptedContacts => allow_creation = false,
|
||||
ShowEmails::All => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,18 +340,16 @@ fn add_parts(
|
||||
let to_id: u32;
|
||||
|
||||
if incoming {
|
||||
state = if 0 != flags & DC_IMAP_SEEN {
|
||||
state = if seen {
|
||||
MessageState::InSeen
|
||||
} else {
|
||||
MessageState::InFresh
|
||||
};
|
||||
to_id = DC_CONTACT_ID_SELF;
|
||||
|
||||
// handshake messages must be processed _before_ chats are created
|
||||
// (eg. contacs may be marked as verified)
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
if mime_parser.get(HeaderDef::SecureJoin).is_some() {
|
||||
// avoid discarding by show_emails setting
|
||||
msgrmsg = MessengerMessage::Yes;
|
||||
msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting
|
||||
*chat_id = ChatId::new(0);
|
||||
allow_creation = true;
|
||||
match handle_securejoin_handshake(context, mime_parser, from_id) {
|
||||
@@ -348,8 +363,7 @@ fn add_parts(
|
||||
state = MessageState::InSeen;
|
||||
}
|
||||
Ok(securejoin::HandshakeMessage::Propagate) => {
|
||||
// Message will still be processed as "member
|
||||
// added" or similar system message.
|
||||
// process messages as "member added" normally
|
||||
}
|
||||
Err(err) => {
|
||||
*hidden = true;
|
||||
@@ -379,7 +393,11 @@ fn add_parts(
|
||||
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_group(
|
||||
context,
|
||||
&mut mime_parser,
|
||||
allow_creation,
|
||||
if test_normal_chat_id.is_unset() {
|
||||
allow_creation
|
||||
} else {
|
||||
true
|
||||
},
|
||||
create_blocked,
|
||||
from_id,
|
||||
to_ids,
|
||||
@@ -390,7 +408,7 @@ fn add_parts(
|
||||
&& chat_id_blocked != Blocked::Not
|
||||
&& create_blocked == Blocked::Not
|
||||
{
|
||||
chat::unblock(context, new_chat_id);
|
||||
new_chat_id.unblock(context);
|
||||
chat_id_blocked = Blocked::Not;
|
||||
}
|
||||
}
|
||||
@@ -423,7 +441,7 @@ fn add_parts(
|
||||
}
|
||||
if !chat_id.is_unset() && Blocked::Not != chat_id_blocked {
|
||||
if Blocked::Not == create_blocked {
|
||||
chat::unblock(context, *chat_id);
|
||||
chat_id.unblock(context);
|
||||
chat_id_blocked = Blocked::Not;
|
||||
} else if is_reply_to_known_message(context, mime_parser) {
|
||||
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
|
||||
@@ -462,6 +480,27 @@ fn add_parts(
|
||||
// We cannot recreate other states (read, error).
|
||||
state = MessageState::OutDelivered;
|
||||
to_id = to_ids.get_index(0).cloned().unwrap_or_default();
|
||||
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
if mime_parser.get(HeaderDef::SecureJoin).is_some() {
|
||||
msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting
|
||||
*chat_id = ChatId::new(0);
|
||||
allow_creation = true;
|
||||
match observe_securejoin_on_other_device(context, mime_parser, to_id) {
|
||||
Ok(securejoin::HandshakeMessage::Done)
|
||||
| Ok(securejoin::HandshakeMessage::Ignore) => {
|
||||
*hidden = true;
|
||||
}
|
||||
Ok(securejoin::HandshakeMessage::Propagate) => {
|
||||
// process messages as "member added" normally
|
||||
}
|
||||
Err(err) => {
|
||||
*hidden = true;
|
||||
error!(context, "Error in Secure-Join watching: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !to_ids.is_empty() {
|
||||
if chat_id.is_unset() {
|
||||
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_group(
|
||||
@@ -476,7 +515,7 @@ fn add_parts(
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
// automatically unblock chat when the user sends a message
|
||||
if !chat_id.is_unset() && chat_id_blocked != Blocked::Not {
|
||||
chat::unblock(context, new_chat_id);
|
||||
new_chat_id.unblock(context);
|
||||
chat_id_blocked = Blocked::Not;
|
||||
}
|
||||
}
|
||||
@@ -497,7 +536,7 @@ fn add_parts(
|
||||
&& Blocked::Not != chat_id_blocked
|
||||
&& Blocked::Not == create_blocked
|
||||
{
|
||||
chat::unblock(context, *chat_id);
|
||||
chat_id.unblock(context);
|
||||
chat_id_blocked = Blocked::Not;
|
||||
}
|
||||
}
|
||||
@@ -508,7 +547,7 @@ fn add_parts(
|
||||
|
||||
if chat_id.is_unset() && self_sent {
|
||||
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
|
||||
// maybe an Autocrypt Setup Messag
|
||||
// maybe an Autocrypt Setup Message
|
||||
let (id, bl) =
|
||||
chat::create_or_lookup_by_contact_id(context, DC_CONTACT_ID_SELF, Blocked::Not)
|
||||
.unwrap_or_default();
|
||||
@@ -516,7 +555,7 @@ fn add_parts(
|
||||
chat_id_blocked = bl;
|
||||
|
||||
if !chat_id.is_unset() && Blocked::Not != chat_id_blocked {
|
||||
chat::unblock(context, *chat_id);
|
||||
chat_id.unblock(context);
|
||||
chat_id_blocked = Blocked::Not;
|
||||
}
|
||||
}
|
||||
@@ -531,14 +570,14 @@ fn add_parts(
|
||||
*chat_id,
|
||||
from_id,
|
||||
*sent_timestamp,
|
||||
0 == flags & DC_IMAP_SEEN,
|
||||
!seen,
|
||||
&mut sort_timestamp,
|
||||
sent_timestamp,
|
||||
&mut rcvd_timestamp,
|
||||
);
|
||||
|
||||
// unarchive chat
|
||||
chat::unarchive(context, *chat_id)?;
|
||||
chat_id.unarchive(context)?;
|
||||
|
||||
// if the mime-headers should be saved, find out its size
|
||||
// (the mime-header ends with an empty line)
|
||||
@@ -569,10 +608,13 @@ fn add_parts(
|
||||
let subject = mime_parser.get_subject().unwrap_or_default();
|
||||
|
||||
for part in mime_parser.parts.iter_mut() {
|
||||
if mime_parser.location_kml.is_some()
|
||||
let is_mdn = !mime_parser.reports.is_empty();
|
||||
|
||||
let is_location_kml = mime_parser.location_kml.is_some()
|
||||
&& icnt == 1
|
||||
&& (part.msg == "-location-" || part.msg.is_empty())
|
||||
{
|
||||
&& (part.msg == "-location-" || part.msg.is_empty());
|
||||
|
||||
if is_mdn || is_location_kml {
|
||||
*hidden = true;
|
||||
if state == MessageState::InFresh {
|
||||
state = MessageState::InNoticed;
|
||||
@@ -850,21 +892,21 @@ fn create_or_lookup_group(
|
||||
|
||||
mime_parser.is_system_message = SystemMessage::GroupNameChanged;
|
||||
} else if let Some(value) = mime_parser.get(HeaderDef::ChatContent) {
|
||||
if value == "group-avatar-changed" && mime_parser.group_avatar != AvatarAction::None
|
||||
{
|
||||
// this is just an explicit message containing the group-avatar,
|
||||
// apart from that, the group-avatar is send along with various other messages
|
||||
mime_parser.is_system_message = SystemMessage::GroupImageChanged;
|
||||
better_msg = context.stock_system_msg(
|
||||
if mime_parser.group_avatar == AvatarAction::Delete {
|
||||
StockMessage::MsgGrpImgDeleted
|
||||
} else {
|
||||
StockMessage::MsgGrpImgChanged
|
||||
},
|
||||
"",
|
||||
"",
|
||||
from_id as u32,
|
||||
)
|
||||
if value == "group-avatar-changed" {
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
// this is just an explicit message containing the group-avatar,
|
||||
// apart from that, the group-avatar is send along with various other messages
|
||||
mime_parser.is_system_message = SystemMessage::GroupImageChanged;
|
||||
better_msg = context.stock_system_msg(
|
||||
match avatar_action {
|
||||
AvatarAction::Delete => StockMessage::MsgGrpImgDeleted,
|
||||
AvatarAction::Change(_) => StockMessage::MsgGrpImgChanged,
|
||||
},
|
||||
"",
|
||||
"",
|
||||
from_id as u32,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -975,7 +1017,7 @@ fn create_or_lookup_group(
|
||||
// XXX insert code in a different PR :)
|
||||
|
||||
// execute group commands
|
||||
if X_MrAddToGrp.is_some() || X_MrRemoveFromGrp.is_some() {
|
||||
if X_MrAddToGrp.is_some() {
|
||||
recreate_member_list = true;
|
||||
} else if X_MrGrpNameChanged {
|
||||
if let Some(ref grpname) = grpname {
|
||||
@@ -994,17 +1036,16 @@ fn create_or_lookup_group(
|
||||
}
|
||||
}
|
||||
}
|
||||
if mime_parser.group_avatar != AvatarAction::None {
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
info!(context, "group-avatar change for {}", chat_id);
|
||||
if let Ok(mut chat) = Chat::load_from_db(context, chat_id) {
|
||||
match &mime_parser.group_avatar {
|
||||
match avatar_action {
|
||||
AvatarAction::Change(profile_image) => {
|
||||
chat.param.set(Param::ProfileImage, profile_image);
|
||||
}
|
||||
AvatarAction::Delete => {
|
||||
chat.param.remove(Param::ProfileImage);
|
||||
}
|
||||
AvatarAction::None => {}
|
||||
};
|
||||
chat.update_param(context)?;
|
||||
send_EVENT_CHAT_MODIFIED = true;
|
||||
@@ -1012,39 +1053,43 @@ fn create_or_lookup_group(
|
||||
}
|
||||
|
||||
// add members to group/check members
|
||||
// for recreation: we should add a timestamp
|
||||
if recreate_member_list {
|
||||
// TODO: the member list should only be recreated if the corresponding message is newer
|
||||
// than the one that is responsible for the current member list, see
|
||||
// https://github.com/deltachat/deltachat-core/issues/127
|
||||
if !chat::is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) {
|
||||
// Members could have been removed while we were
|
||||
// absent. We can't use existing member list and need to
|
||||
// start from scratch.
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM chats_contacts WHERE chat_id=?;",
|
||||
params![chat_id],
|
||||
)
|
||||
.ok();
|
||||
|
||||
let skip = X_MrRemoveFromGrp.as_ref();
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM chats_contacts WHERE chat_id=?;",
|
||||
params![chat_id],
|
||||
)
|
||||
.ok();
|
||||
if skip.is_none() || !addr_cmp(&self_addr, skip.unwrap()) {
|
||||
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF);
|
||||
}
|
||||
if from_id > DC_CHAT_ID_LAST_SPECIAL
|
||||
if from_id > DC_CONTACT_ID_LAST_SPECIAL
|
||||
&& !Contact::addr_equals_contact(context, &self_addr, from_id as u32)
|
||||
&& (skip.is_none()
|
||||
|| !Contact::addr_equals_contact(context, skip.unwrap(), from_id as u32))
|
||||
&& !chat::is_contact_in_chat(context, chat_id, from_id)
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, chat_id, from_id as u32);
|
||||
}
|
||||
for &to_id in to_ids.iter() {
|
||||
info!(context, "adding to={:?} to chat id={}", to_id, chat_id);
|
||||
if !Contact::addr_equals_contact(context, &self_addr, to_id)
|
||||
&& (skip.is_none() || !Contact::addr_equals_contact(context, skip.unwrap(), to_id))
|
||||
&& !chat::is_contact_in_chat(context, chat_id, to_id)
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, chat_id, to_id);
|
||||
}
|
||||
}
|
||||
send_EVENT_CHAT_MODIFIED = true;
|
||||
} else if let Some(removed_addr) = X_MrRemoveFromGrp {
|
||||
let contact_id = Contact::lookup_id_by_addr(context, removed_addr);
|
||||
if contact_id != 0 {
|
||||
info!(context, "remove {:?} from chat id={}", contact_id, chat_id);
|
||||
chat::remove_from_chat_contacts_table(context, chat_id, contact_id);
|
||||
}
|
||||
send_EVENT_CHAT_MODIFIED = true;
|
||||
}
|
||||
|
||||
if send_EVENT_CHAT_MODIFIED {
|
||||
@@ -1054,7 +1099,7 @@ fn create_or_lookup_group(
|
||||
}
|
||||
|
||||
/// try extract a grpid from a message-id list header value
|
||||
fn extract_grpid<'a>(mime_parser: &'a MimeMessage, headerdef: HeaderDef) -> Option<&'a str> {
|
||||
fn extract_grpid(mime_parser: &MimeMessage, headerdef: HeaderDef) -> Option<&str> {
|
||||
let header = mime_parser.get(headerdef)?;
|
||||
let parts = header
|
||||
.split(',')
|
||||
@@ -1462,7 +1507,7 @@ fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Ok(ids) = mailparse::addrparse(mid_list) {
|
||||
if let Ok(ids) = mailparse::msgidparse(mid_list) {
|
||||
for id in ids.iter() {
|
||||
if is_known_rfc724_mid(context, id) {
|
||||
return true;
|
||||
@@ -1474,8 +1519,8 @@ fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Check if a message is a reply to a known message (messenger or non-messenger).
|
||||
fn is_known_rfc724_mid(context: &Context, rfc724_mid: &mailparse::MailAddr) -> bool {
|
||||
let addr = extract_single_from_addr(rfc724_mid);
|
||||
fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
@@ -1483,7 +1528,7 @@ fn is_known_rfc724_mid(context: &Context, rfc724_mid: &mailparse::MailAddr) -> b
|
||||
LEFT JOIN chats c ON m.chat_id=c.id \
|
||||
WHERE m.rfc724_mid=? \
|
||||
AND m.chat_id>9 AND c.blocked=0;",
|
||||
params![addr],
|
||||
params![rfc724_mid],
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -1509,8 +1554,8 @@ fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) -
|
||||
false
|
||||
}
|
||||
|
||||
fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
||||
if let Ok(ids) = mailparse::addrparse(mid_list) {
|
||||
pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
||||
if let Ok(ids) = mailparse::msgidparse(mid_list) {
|
||||
for id in ids.iter() {
|
||||
if is_msgrmsg_rfc724_mid(context, id) {
|
||||
return true;
|
||||
@@ -1520,21 +1565,14 @@ fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn extract_single_from_addr(addr: &mailparse::MailAddr) -> &String {
|
||||
match addr {
|
||||
mailparse::MailAddr::Group(infos) => &infos.addrs[0].addr,
|
||||
mailparse::MailAddr::Single(info) => &info.addr,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a message is a reply to any messenger message.
|
||||
fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &mailparse::MailAddr) -> bool {
|
||||
let addr = extract_single_from_addr(rfc724_mid);
|
||||
fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM msgs WHERE rfc724_mid=? AND msgrmsg!=0 AND chat_id>9;",
|
||||
params![addr],
|
||||
params![rfc724_mid],
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -1619,7 +1657,9 @@ fn dc_create_incoming_rfc724_mid(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::dummy_context;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::{dummy_context, TestContext};
|
||||
|
||||
#[test]
|
||||
fn test_hex_hash() {
|
||||
@@ -1678,4 +1718,170 @@ mod tests {
|
||||
Some("123-45-9@stub".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_known_rfc724_mid() {
|
||||
let t = dummy_context();
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("first message".to_string());
|
||||
let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap();
|
||||
let msg = Message::load_from_db(&t.ctx, msg_id).unwrap();
|
||||
|
||||
// Message-IDs may or may not be surrounded by angle brackets
|
||||
assert!(is_known_rfc724_mid(
|
||||
&t.ctx,
|
||||
format!("<{}>", msg.rfc724_mid).as_str()
|
||||
));
|
||||
assert!(is_known_rfc724_mid(&t.ctx, &msg.rfc724_mid));
|
||||
assert!(!is_known_rfc724_mid(&t.ctx, "nonexistant@message.id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_msgrmsg_rfc724_mid() {
|
||||
let t = dummy_context();
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("first message".to_string());
|
||||
let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap();
|
||||
let msg = Message::load_from_db(&t.ctx, msg_id).unwrap();
|
||||
|
||||
// Message-IDs may or may not be surrounded by angle brackets
|
||||
assert!(is_msgrmsg_rfc724_mid(
|
||||
&t.ctx,
|
||||
format!("<{}>", msg.rfc724_mid).as_str()
|
||||
));
|
||||
assert!(is_msgrmsg_rfc724_mid(&t.ctx, &msg.rfc724_mid));
|
||||
assert!(!is_msgrmsg_rfc724_mid(&t.ctx, "nonexistant@message.id"));
|
||||
}
|
||||
|
||||
fn configured_offline_context() -> TestContext {
|
||||
let t = dummy_context();
|
||||
t.ctx
|
||||
.set_config(Config::Addr, Some("alice@example.org"))
|
||||
.unwrap();
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.unwrap();
|
||||
t.ctx.set_config(Config::Configured, Some("1")).unwrap();
|
||||
t
|
||||
}
|
||||
|
||||
static MSGRMSG: &[u8] = b"From: Bob <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Chat: hello\n\
|
||||
Message-ID: <Mr.1111@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n";
|
||||
|
||||
static ONETOONE_NOREPLY_MAIL: &[u8] = b"From: Bob <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: Chat: hello\n\
|
||||
Message-ID: <2222@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
|
||||
\n\
|
||||
hello\n";
|
||||
|
||||
static GRP_MAIL: &[u8] = b"From: bob@example.org\n\
|
||||
To: alice@example.org, claire@example.org\n\
|
||||
Subject: group with Alice, Bob and Claire\n\
|
||||
Message-ID: <3333@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n";
|
||||
|
||||
#[test]
|
||||
fn test_adhoc_group_show_chats_only() {
|
||||
let t = configured_offline_context();
|
||||
assert_eq!(t.ctx.get_config_int(Config::ShowEmails), 0);
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
dc_receive_imf(&t.ctx, MSGRMSG, "INBOX", 1, false).unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
dc_receive_imf(&t.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 1, false).unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adhoc_group_show_accepted_contact_unknown() {
|
||||
let t = configured_offline_context();
|
||||
t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap();
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
|
||||
|
||||
// adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adhoc_group_show_accepted_contact_known() {
|
||||
let t = configured_offline_context();
|
||||
t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap();
|
||||
Contact::create(&t.ctx, "Bob", "bob@example.org").unwrap();
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
|
||||
|
||||
// adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts
|
||||
// (and existent chat is required)
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adhoc_group_show_accepted_contact_accepted() {
|
||||
let t = configured_offline_context();
|
||||
t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap();
|
||||
|
||||
// accept Bob by accepting a delta-message from Bob
|
||||
dc_receive_imf(&t.ctx, MSGRMSG, "INBOX", 1, false).unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert!(chats.get_chat_id(0).is_deaddrop());
|
||||
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
|
||||
assert!(!chat_id.is_special());
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert_eq!(chat.name, "Bob");
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 1);
|
||||
assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).len(), 1);
|
||||
|
||||
// receive a non-delta-message from Bob, shows up because of the show_emails setting
|
||||
dc_receive_imf(&t.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 2, false).unwrap();
|
||||
assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).len(), 2);
|
||||
|
||||
// let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 3, false).unwrap();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 2);
|
||||
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat.name, "group with Alice, Bob and Claire");
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adhoc_group_show_all() {
|
||||
let t = configured_offline_context();
|
||||
t.ctx.set_config(Config::ShowEmails, Some("2")).unwrap();
|
||||
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
|
||||
|
||||
// adhoc-group with unknown contacts with show_emails=all will show up in the deaddrop
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert!(chats.get_chat_id(0).is_deaddrop());
|
||||
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat.name, "group with Alice, Bob and Claire");
|
||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
127
src/dc_tools.rs
127
src/dc_tools.rs
@@ -19,10 +19,10 @@ pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
||||
0 != v && 0 == v & (v - 1)
|
||||
}
|
||||
|
||||
/// Shortens a string to a specified length and adds "..." or "[...]" to the end of
|
||||
/// the shortened string.
|
||||
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Cow<str> {
|
||||
let ellipse = if do_unwrap { "..." } else { "[...]" };
|
||||
/// Shortens a string to a specified length and adds "[...]" to the
|
||||
/// end of the shortened string.
|
||||
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
|
||||
let ellipse = "[...]";
|
||||
|
||||
let count = buf.chars().count();
|
||||
if approx_chars > 0 && count > approx_chars + ellipse.len() {
|
||||
@@ -222,43 +222,6 @@ pub(crate) fn dc_extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
// Function returns a sanitized basename that does not contain
|
||||
// win/linux path separators and also not any non-ascii chars
|
||||
fn get_safe_basename(filename: &str) -> String {
|
||||
// return the (potentially mangled) basename of the input filename
|
||||
// this might be a path that comes in from another operating system
|
||||
let mut index: usize = 0;
|
||||
|
||||
if let Some(unix_index) = filename.rfind('/') {
|
||||
index = unix_index + 1;
|
||||
}
|
||||
if let Some(win_index) = filename.rfind('\\') {
|
||||
index = max(index, win_index + 1);
|
||||
}
|
||||
if index >= filename.len() {
|
||||
"nobasename".to_string()
|
||||
} else {
|
||||
// we don't allow any non-ascii to be super-safe
|
||||
filename[index..].replace(|c: char| !c.is_ascii() || c == ':', "-")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dc_derive_safe_stem_ext(filename: &str) -> (String, String) {
|
||||
let basename = get_safe_basename(&filename);
|
||||
let (mut stem, mut ext) = if let Some(index) = basename.rfind('.') {
|
||||
(
|
||||
basename[0..index].to_string(),
|
||||
basename[index..].to_string(),
|
||||
)
|
||||
} else {
|
||||
(basename, "".to_string())
|
||||
};
|
||||
// limit length of stem and ext
|
||||
stem.truncate(32);
|
||||
ext.truncate(32);
|
||||
(stem, ext)
|
||||
}
|
||||
|
||||
// the returned suffix is lower-case
|
||||
pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
|
||||
Path::new(path_filename.as_ref())
|
||||
@@ -505,7 +468,7 @@ pub(crate) fn time() -> i64 {
|
||||
/// assert_eq!(&email.domain, "example.com");
|
||||
/// assert_eq!(email.to_string(), "someone@example.com");
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct EmailAddress {
|
||||
pub local: String,
|
||||
pub domain: String,
|
||||
@@ -575,54 +538,42 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dc_truncate_1() {
|
||||
let s = "this is a little test string";
|
||||
assert_eq!(dc_truncate(s, 16, false), "this is a [...]");
|
||||
assert_eq!(dc_truncate(s, 16, true), "this is a ...");
|
||||
assert_eq!(dc_truncate(s, 16), "this is a [...]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_2() {
|
||||
assert_eq!(dc_truncate("1234", 2, false), "1234");
|
||||
assert_eq!(dc_truncate("1234", 2, true), "1234");
|
||||
assert_eq!(dc_truncate("1234", 2), "1234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_3() {
|
||||
assert_eq!(dc_truncate("1234567", 1, false), "1[...]");
|
||||
assert_eq!(dc_truncate("1234567", 1, true), "1...");
|
||||
assert_eq!(dc_truncate("1234567", 1), "1[...]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_4() {
|
||||
assert_eq!(dc_truncate("123456", 4, false), "123456");
|
||||
assert_eq!(dc_truncate("123456", 4, true), "123456");
|
||||
assert_eq!(dc_truncate("123456", 4), "123456");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_truncate_edge() {
|
||||
assert_eq!(dc_truncate("", 4, false), "");
|
||||
assert_eq!(dc_truncate("", 4, true), "");
|
||||
assert_eq!(dc_truncate("", 4), "");
|
||||
|
||||
assert_eq!(dc_truncate("\n hello \n world", 4, false), "\n [...]");
|
||||
assert_eq!(dc_truncate("\n hello \n world", 4, true), "\n ...");
|
||||
assert_eq!(dc_truncate("\n hello \n world", 4), "\n [...]");
|
||||
|
||||
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
|
||||
assert_eq!(
|
||||
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1, false),
|
||||
"𐠈[...]"
|
||||
);
|
||||
assert_eq!(
|
||||
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0, false),
|
||||
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0),
|
||||
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
|
||||
);
|
||||
|
||||
// 9 characters, so no truncation
|
||||
assert_eq!(
|
||||
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6, false),
|
||||
"𑒀ὐ¢🜀\u{1e01b}A a🟠",
|
||||
);
|
||||
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
|
||||
|
||||
// 12 characters, truncation
|
||||
assert_eq!(
|
||||
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6, false),
|
||||
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6),
|
||||
"𑒀ὐ¢🜀\u{1e01b}A[...]",
|
||||
);
|
||||
}
|
||||
@@ -707,29 +658,29 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_emailaddress_parse() {
|
||||
assert_eq!(EmailAddress::new("").is_ok(), false);
|
||||
assert_eq!("".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!(
|
||||
EmailAddress::new("user@domain.tld").unwrap(),
|
||||
"user@domain.tld".parse::<EmailAddress>().unwrap(),
|
||||
EmailAddress {
|
||||
local: "user".into(),
|
||||
domain: "domain.tld".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
|
||||
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
|
||||
assert_eq!(EmailAddress::new("tt.dd@uu").is_ok(), false);
|
||||
assert_eq!(EmailAddress::new("u@d").is_ok(), false);
|
||||
assert_eq!(EmailAddress::new("u@d.").is_ok(), false);
|
||||
assert_eq!(EmailAddress::new("u@d.t").is_ok(), false);
|
||||
assert_eq!("uuu".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!("dd.tt".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!("tt.dd@uu".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!("u@d".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!("u@d.".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!("u@d.t".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!(
|
||||
EmailAddress::new("u@d.tt").unwrap(),
|
||||
"u@d.tt".parse::<EmailAddress>().unwrap(),
|
||||
EmailAddress {
|
||||
local: "u".into(),
|
||||
domain: "d.tt".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(EmailAddress::new("u@.tt").is_ok(), false);
|
||||
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
|
||||
assert_eq!("u@tt".parse::<EmailAddress>().is_ok(), false);
|
||||
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
|
||||
}
|
||||
|
||||
use proptest::prelude::*;
|
||||
@@ -738,11 +689,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dc_truncate(
|
||||
buf: String,
|
||||
approx_chars in 0..10000usize,
|
||||
do_unwrap: bool,
|
||||
approx_chars in 0..10000usize
|
||||
) {
|
||||
let res = dc_truncate(&buf, approx_chars, do_unwrap);
|
||||
let el_len = if do_unwrap { 3 } else { 5 };
|
||||
let res = dc_truncate(&buf, approx_chars);
|
||||
let el_len = 5;
|
||||
let l = res.chars().count();
|
||||
if approx_chars > 0 {
|
||||
assert!(
|
||||
@@ -756,28 +706,11 @@ mod tests {
|
||||
|
||||
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
|
||||
let l = res.len();
|
||||
if do_unwrap {
|
||||
assert_eq!(&res[l-3..l], "...", "missing ellipsis in {}", &res);
|
||||
} else {
|
||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
||||
}
|
||||
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_get_safe_basename() {
|
||||
assert_eq!(get_safe_basename("12312/hello"), "hello");
|
||||
assert_eq!(get_safe_basename("12312\\hello"), "hello");
|
||||
assert_eq!(get_safe_basename("//12312\\hello"), "hello");
|
||||
assert_eq!(get_safe_basename("//123:12\\hello"), "hello");
|
||||
assert_eq!(get_safe_basename("//123:12/\\\\hello"), "hello");
|
||||
assert_eq!(get_safe_basename("//123:12//hello"), "hello");
|
||||
assert_eq!(get_safe_basename("//123:12//"), "nobasename");
|
||||
assert_eq!(get_safe_basename("//123:12/"), "nobasename");
|
||||
assert!(get_safe_basename("123\x012.hello").ends_with(".hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_handling() {
|
||||
let t = dummy_context();
|
||||
|
||||
61
src/e2ee.rs
61
src/e2ee.rs
@@ -1,15 +1,19 @@
|
||||
//! End-to-end encryption support.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use mailparse::{MailHeaderMap, ParsedMail};
|
||||
use mailparse::ParsedMail;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::*;
|
||||
use crate::config::Config;
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::error::*;
|
||||
use crate::key::*;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{self, Key, KeyPairUse, SignedPublicKey};
|
||||
use crate::keyring::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::pgp;
|
||||
@@ -19,7 +23,7 @@ use crate::securejoin::handle_degrade_event;
|
||||
pub struct EncryptHelper {
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
pub addr: String,
|
||||
pub public_key: Key,
|
||||
pub public_key: SignedPublicKey,
|
||||
}
|
||||
|
||||
impl EncryptHelper {
|
||||
@@ -102,8 +106,8 @@ impl EncryptHelper {
|
||||
})?;
|
||||
keyring.add_ref(key);
|
||||
}
|
||||
|
||||
keyring.add_ref(&self.public_key);
|
||||
let public_key = Key::from(self.public_key.clone());
|
||||
keyring.add_ref(&public_key);
|
||||
let sign_key = Key::from_self_private(context, self.addr.clone(), &context.sql)
|
||||
.ok_or_else(|| format_err!("missing own private key"))?;
|
||||
|
||||
@@ -122,7 +126,7 @@ pub fn try_decrypt(
|
||||
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
|
||||
let from = mail
|
||||
.headers
|
||||
.get_first_value("From")?
|
||||
.get_header_value(HeaderDef::From_)
|
||||
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
|
||||
.and_then(|from| from.extract_single_info())
|
||||
.map(|from| from.addr)
|
||||
@@ -191,44 +195,35 @@ pub fn try_decrypt(
|
||||
/// storing a new one when one doesn't exist yet. Care is taken to
|
||||
/// only generate one key per context even when multiple threads call
|
||||
/// this function concurrently.
|
||||
fn load_or_generate_self_public_key(context: &Context, self_addr: impl AsRef<str>) -> Result<Key> {
|
||||
fn load_or_generate_self_public_key(
|
||||
context: &Context,
|
||||
self_addr: impl AsRef<str>,
|
||||
) -> Result<SignedPublicKey> {
|
||||
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
|
||||
return Ok(key);
|
||||
return SignedPublicKey::try_from(key)
|
||||
.map_err(|_| Error::Message("Not a public key".into()));
|
||||
}
|
||||
let _guard = context.generating_key_mutex.lock().unwrap();
|
||||
|
||||
// Check again in case the key was generated while we were waiting for the lock.
|
||||
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
|
||||
return Ok(key);
|
||||
return SignedPublicKey::try_from(key)
|
||||
.map_err(|_| Error::Message("Not a public key".into()));
|
||||
}
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let keygen_type =
|
||||
KeyGenType::from_i32(context.get_config_int(Config::KeyGenType)).unwrap_or_default();
|
||||
info!(context, "Generating keypair with type {}", keygen_type);
|
||||
let keypair = pgp::create_keypair(EmailAddress::new(self_addr.as_ref())?, keygen_type)?;
|
||||
key::store_self_keypair(context, &keypair, KeyPairUse::Default)?;
|
||||
info!(
|
||||
context,
|
||||
"Generating keypair with {} bits, e={} ...", 2048, 65537,
|
||||
"Keypair generated in {:.3}s.",
|
||||
start.elapsed().as_secs()
|
||||
);
|
||||
match pgp::create_keypair(&self_addr) {
|
||||
Some((public_key, private_key)) => {
|
||||
if dc_key_save_self_keypair(
|
||||
context,
|
||||
&public_key,
|
||||
&private_key,
|
||||
&self_addr,
|
||||
true,
|
||||
&context.sql,
|
||||
) {
|
||||
info!(
|
||||
context,
|
||||
"Keypair generated in {:.3}s.",
|
||||
start.elapsed().as_secs()
|
||||
);
|
||||
Ok(public_key)
|
||||
} else {
|
||||
Err(format_err!("Failed to save keypair"))
|
||||
}
|
||||
}
|
||||
None => Err(format_err!("Failed to generate keypair")),
|
||||
}
|
||||
Ok(keypair.public)
|
||||
}
|
||||
|
||||
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
|
||||
@@ -422,7 +417,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // generating keys is expensive
|
||||
fn test_generate() {
|
||||
let t = dummy_context();
|
||||
let addr = "alice@example.org";
|
||||
@@ -434,7 +428,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_generate_concurrent() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
15
src/error.rs
15
src/error.rs
@@ -16,6 +16,9 @@ pub enum Error {
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Message(String),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
MessageWithCause(String, #[cause] failure::Error, failure::Backtrace),
|
||||
|
||||
#[fail(display = "{:?}", _0)]
|
||||
Image(image_meta::ImageError),
|
||||
|
||||
@@ -118,6 +121,18 @@ impl From<crate::message::InvalidMsgId> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::key::SaveKeyError> for Error {
|
||||
fn from(err: crate::key::SaveKeyError) -> Error {
|
||||
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::pgp::PgpKeygenError> for Error {
|
||||
fn from(err: crate::pgp::PgpKeygenError) -> Error {
|
||||
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mailparse::MailParseError> for Error {
|
||||
fn from(err: mailparse::MailParseError) -> Error {
|
||||
Error::MailParseError(err)
|
||||
|
||||
345
src/export_chat.rs
Normal file
345
src/export_chat.rs
Normal file
@@ -0,0 +1,345 @@
|
||||
// use crate::dc_tools::*;
|
||||
use crate::chat::*;
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::error::Error;
|
||||
use crate::message::*;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::path::Path;
|
||||
use zip::write::FileOptions;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExportChatResult {
|
||||
html: String,
|
||||
referenced_blobs: Vec<String>,
|
||||
}
|
||||
|
||||
struct ContactInfo {
|
||||
name: String,
|
||||
initial: String,
|
||||
color: String,
|
||||
profile_img: Option<String>,
|
||||
}
|
||||
|
||||
pub fn pack_exported_chat(
|
||||
context: &Context,
|
||||
artifact: ExportChatResult,
|
||||
filename: &str,
|
||||
) -> zip::result::ZipResult<()> {
|
||||
let path = std::path::Path::new(filename);
|
||||
let file = std::fs::File::create(&path).unwrap();
|
||||
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
|
||||
zip.start_file("index.html", Default::default())?;
|
||||
zip.write_all(artifact.html.as_bytes())?;
|
||||
|
||||
zip.start_file("styles.css", Default::default())?;
|
||||
zip.write_all(include_bytes!("../assets/exported-chat.css"))?;
|
||||
|
||||
zip.add_directory("blobs/", Default::default())?;
|
||||
|
||||
let options = FileOptions::default();
|
||||
for blob_name in artifact.referenced_blobs {
|
||||
let path = context.get_blobdir().join(&blob_name);
|
||||
|
||||
// println!("adding file {:?} as {:?} ...", path, &blob_name);
|
||||
zip.start_file_from_path(Path::new(&format!("blobs/{}", &blob_name)), options)?;
|
||||
let mut f = File::open(path)?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
f.read_to_end(&mut buffer)?;
|
||||
zip.write_all(&*buffer)?;
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
zip.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn export_chat(context: &Context, chat_id: ChatId) -> ExportChatResult {
|
||||
let mut blobs = Vec::new();
|
||||
let mut chat_author_ids = Vec::new();
|
||||
// get all messages
|
||||
let messages: Vec<std::result::Result<Message, Error>> =
|
||||
get_chat_msgs(context, chat_id, 0, None)
|
||||
.into_iter()
|
||||
.map(|msg_id| Message::load_from_db(context, msg_id))
|
||||
.collect();
|
||||
// push all referenced blobs and populate contactid list
|
||||
for message in &messages {
|
||||
if let Ok(msg) = &message {
|
||||
let filename = msg.get_filename();
|
||||
if let Some(file) = filename {
|
||||
// push referenced blobs (attachments)
|
||||
blobs.push(file);
|
||||
}
|
||||
chat_author_ids.push(msg.from_id);
|
||||
}
|
||||
}
|
||||
// deduplicate contact list and load the contacts
|
||||
chat_author_ids.dedup();
|
||||
// chache information about the authors
|
||||
let mut chat_authors: HashMap<u32, ContactInfo> = HashMap::new();
|
||||
chat_authors.insert(
|
||||
0,
|
||||
ContactInfo {
|
||||
name: "Err: Contact not found".to_owned(),
|
||||
initial: "#".to_owned(),
|
||||
profile_img: None,
|
||||
color: "grey".to_owned(),
|
||||
},
|
||||
);
|
||||
for author_id in chat_author_ids {
|
||||
let contact = Contact::get_by_id(context, author_id);
|
||||
if let Ok(c) = contact {
|
||||
let profile_img_path: String;
|
||||
if let Some(path) = c.get_profile_image(context) {
|
||||
profile_img_path = path
|
||||
.file_name()
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new(""))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
// push referenced blobs (avatars)
|
||||
blobs.push(profile_img_path.clone());
|
||||
} else {
|
||||
profile_img_path = "".to_owned();
|
||||
}
|
||||
chat_authors.insert(
|
||||
author_id,
|
||||
ContactInfo {
|
||||
name: c.get_display_name().to_owned(),
|
||||
initial: "#".to_owned(), // TODO
|
||||
profile_img: match profile_img_path != "" {
|
||||
true => Some(profile_img_path),
|
||||
false => None,
|
||||
},
|
||||
color: "rgb(18, 126, 208)".to_owned(), // TODO
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// run message_to_html for each message and generate the html that way
|
||||
let mut html_messages: Vec<String> = Vec::new();
|
||||
for message in messages {
|
||||
if let Ok(msg) = message {
|
||||
html_messages.push(message_to_html(&chat_authors, msg, context));
|
||||
} else {
|
||||
html_messages.push(format!(
|
||||
r#"<li>
|
||||
<div class='message error'>
|
||||
<div class="msg-container">
|
||||
<div class="msg-body">
|
||||
<div dir="auto" class="text">{:?}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>"#,
|
||||
message.unwrap_err()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// todo chat image, chat name and so on..
|
||||
let chat = Chat::load_from_db(context, chat_id).unwrap();
|
||||
let chat_avatar = match chat.get_profile_image(context) {
|
||||
Some(img) => {
|
||||
let path = img
|
||||
.file_name()
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new(""))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
blobs.push(path.clone());
|
||||
format!("<img class=\"avatar\" src=\"blobs/{}\" />", path)
|
||||
}
|
||||
None => format!(
|
||||
"<div class=\"avatar text-avatar\" style=\"background-color:#{:#}\">{}</div>",
|
||||
chat.get_color(context),
|
||||
chat.get_name().chars().next().unwrap()
|
||||
),
|
||||
};
|
||||
|
||||
// todo option to export locations as kml?
|
||||
|
||||
// todo export message infos and save them to txt files
|
||||
// (those can be linked from the messages, they are stored in msg_info/[msg-id].txt)
|
||||
|
||||
blobs.dedup();
|
||||
ExportChatResult {
|
||||
html: format!(
|
||||
"<html>\
|
||||
<head>\
|
||||
<title>{chat_name}</title>\
|
||||
<link rel=\"stylesheet\" href=\"styles.css\" type=\"text/css\">\
|
||||
</head>\
|
||||
<body>\
|
||||
<div class=\"header\">\
|
||||
{chat_avatar}\
|
||||
<div class=\"name\">{chat_name}</div>\
|
||||
</div>\
|
||||
<div class=\"message-list-and-composer__message-list\">\
|
||||
<div id=\"message-list\">\
|
||||
<ul>{messages}</ul>\
|
||||
</div>\
|
||||
</div>\
|
||||
</body>\
|
||||
</html>",
|
||||
chat_name = chat.get_name(),
|
||||
chat_avatar = chat_avatar,
|
||||
messages = html_messages.join("")
|
||||
),
|
||||
referenced_blobs: blobs,
|
||||
}
|
||||
}
|
||||
|
||||
fn message_to_html(
|
||||
author_cache: &HashMap<u32, ContactInfo>,
|
||||
message: Message,
|
||||
context: &Context,
|
||||
) -> String {
|
||||
let author: &ContactInfo = {
|
||||
if let Some(c) = author_cache.get(&message.get_from_id()) {
|
||||
c
|
||||
} else {
|
||||
author_cache.get(&0).unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
let avatar: String = {
|
||||
if let Some(profile_img) = &author.profile_img {
|
||||
format!(
|
||||
"<div class=\"author-avatar\">\
|
||||
<img \
|
||||
alt=\"{author_name}\"\
|
||||
src=\"blobs/{author_avatar_src}\"\
|
||||
/>\
|
||||
</div>",
|
||||
author_name = author.name,
|
||||
author_avatar_src = profile_img
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"<div class=\"author-avatar default\" alt=\"{name}\">\
|
||||
<div class=\"label\" style=\"background-color: {color}\">\
|
||||
{initial}\
|
||||
</div>\
|
||||
</div>",
|
||||
name = author.name,
|
||||
initial = author.initial,
|
||||
color = author.color
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// save and refernce message source code somehow?
|
||||
|
||||
let has_text = message.get_text().is_some() && !message.get_text().unwrap().is_empty();
|
||||
|
||||
let attachment = match message.get_file(context) {
|
||||
None => "".to_owned(),
|
||||
Some(file) => {
|
||||
let modifier_class = if has_text { "content-below" } else { "" };
|
||||
let filename = file
|
||||
.file_name()
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new(""))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
match message.get_viewtype() {
|
||||
Viewtype::Audio => {
|
||||
format!("<audio \
|
||||
controls \
|
||||
class=\"message-attachment-audio {}\"> \
|
||||
<source src=\"blobs/{}\" /> \
|
||||
</audio>", modifier_class ,filename)
|
||||
},
|
||||
Viewtype::Gif | Viewtype::Image | Viewtype::Sticker => {
|
||||
format!("<a \
|
||||
href=\"blobs/{filename}\" \
|
||||
role=\"button\" \
|
||||
class=\"message-attachment-media {modifier_class}\"> \
|
||||
<img className='attachment-content' src=\"blobs/{filename}\" /> \
|
||||
</a>", modifier_class=modifier_class, filename=filename)
|
||||
},
|
||||
Viewtype::Video => {
|
||||
format!("<a \
|
||||
href=\"blobs/{filename}\" \
|
||||
role=\"button\" \
|
||||
class=\"message-attachment-media {modifier_class}\"> \
|
||||
<video className='attachment-content' src=\"blobs/{filename}\" controls=\"true\" /> \
|
||||
</a>", modifier_class=modifier_class, filename=filename)
|
||||
},
|
||||
_ => {
|
||||
format!("<div class=\"message-attachment-generic {modifier_class}\">\
|
||||
<div class=\"file-icon\">\
|
||||
<div class=\"file-extension\">\
|
||||
{extension} \
|
||||
</div>\
|
||||
</div>\
|
||||
<div className=\"text-part\">\
|
||||
<a href=\"blobs/{filename}\" className=\"name\">{filename}</a>\
|
||||
<div className=\"size\">{filesize}</div>\
|
||||
</div>\
|
||||
</div>",
|
||||
modifier_class=modifier_class,
|
||||
filename=filename,
|
||||
filesize=message.get_filebytes(&context) /* todo human readable file size*/,
|
||||
extension=file.extension().unwrap_or_else(|| std::ffi::OsStr::new("")).to_str().unwrap().to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
format!(
|
||||
"<li>\
|
||||
<div class=\"message {direction}\">\
|
||||
{avatar}\
|
||||
<div class=\"msg-container\">\
|
||||
<span class=\"author\" style=\"color: {author_color};\">{author_name}</span>\
|
||||
<div class=\"msg-body\">\
|
||||
{attachment}
|
||||
<div dir=\"auto\" class=\"text\">\
|
||||
{content}\
|
||||
</div>\
|
||||
<div class=\"metadata {with_image_no_caption}\">\
|
||||
{encryption}\
|
||||
<span class=\"date date--{direction}\" title=\"{full_time}\">{relative_time}</span>\
|
||||
<span class=\"spacer\"></span>\
|
||||
</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
<div>\
|
||||
</li>",
|
||||
direction = match message.from_id == DC_CONTACT_ID_SELF {
|
||||
true => "outgoing",
|
||||
false => "incoming",
|
||||
},
|
||||
avatar = avatar,
|
||||
author_name = author.name,
|
||||
author_color = author.color,
|
||||
attachment = attachment,
|
||||
content = message.get_text().unwrap_or_else(|| "".to_owned()),
|
||||
with_image_no_caption = if !has_text && message.get_viewtype() == Viewtype::Image {
|
||||
"with-image-no-caption"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
encryption = match message.get_showpadlock() {
|
||||
true => r#"<div aria-label="Encryption padlock" class="padlock-icon"></div>"#,
|
||||
false => "",
|
||||
},
|
||||
full_time = "Tue, Feb 25, 2020 3:49 PM", // message.get_timestamp() ? // todo
|
||||
relative_time = "Tue 3:49 PM" // todo
|
||||
)
|
||||
|
||||
// todo link to raw message data
|
||||
// todo link to message info
|
||||
}
|
||||
|
||||
//TODO tests
|
||||
@@ -1,4 +1,7 @@
|
||||
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames)]
|
||||
use crate::strum::AsStaticRef;
|
||||
use mailparse::{MailHeader, MailHeaderMap};
|
||||
|
||||
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
#[allow(dead_code)]
|
||||
pub enum HeaderDef {
|
||||
@@ -23,7 +26,6 @@ pub enum HeaderDef {
|
||||
ChatGroupName,
|
||||
ChatGroupNameChanged,
|
||||
ChatVerified,
|
||||
ChatGroupImage, // deprecated
|
||||
ChatGroupAvatar,
|
||||
ChatUserAvatar,
|
||||
ChatVoiceMessage,
|
||||
@@ -32,6 +34,7 @@ pub enum HeaderDef {
|
||||
ChatContent,
|
||||
ChatDuration,
|
||||
ChatDispositionNotificationTo,
|
||||
Autocrypt,
|
||||
AutocryptSetupMessage,
|
||||
SecureJoin,
|
||||
SecureJoinGroup,
|
||||
@@ -43,8 +46,18 @@ pub enum HeaderDef {
|
||||
|
||||
impl HeaderDef {
|
||||
/// Returns the corresponding Event id.
|
||||
pub fn get_headername(&self) -> String {
|
||||
self.to_string()
|
||||
pub fn get_headername(&self) -> &'static str {
|
||||
self.as_static()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HeaderDefMap {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
|
||||
}
|
||||
|
||||
impl HeaderDefMap for [MailHeader<'_>] {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
|
||||
self.get_first_value(headerdef.get_headername())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +68,24 @@ mod tests {
|
||||
#[test]
|
||||
/// Test that kebab_case serialization works as expected
|
||||
fn kebab_test() {
|
||||
assert_eq!(HeaderDef::From_.to_string(), "from");
|
||||
assert_eq!(HeaderDef::From_.get_headername(), "from");
|
||||
|
||||
assert_eq!(HeaderDef::_TestHeader.to_string(), "test-header");
|
||||
assert_eq!(HeaderDef::_TestHeader.get_headername(), "test-header");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test that headers are parsed case-insensitively
|
||||
fn test_get_header_value_case() {
|
||||
let (headers, _) =
|
||||
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::AutocryptSetupMessage),
|
||||
Some("v99".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::From_),
|
||||
Some("Bob".to_string())
|
||||
);
|
||||
assert_eq!(headers.get_header_value(HeaderDef::Autocrypt), None);
|
||||
}
|
||||
}
|
||||
|
||||
104
src/imap/client.rs
Normal file
104
src/imap/client.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use async_imap::{
|
||||
error::{Error as ImapError, Result as ImapResult},
|
||||
Client as ImapClient,
|
||||
};
|
||||
use async_native_tls::TlsStream;
|
||||
use async_std::net::{self, TcpStream};
|
||||
|
||||
use super::session::Session;
|
||||
use crate::login_param::{dc_build_tls, CertificateChecks};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Client {
|
||||
Secure(ImapClient<TlsStream<TcpStream>>),
|
||||
Insecure(ImapClient<TcpStream>),
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
|
||||
addr: A,
|
||||
domain: S,
|
||||
certificate_checks: CertificateChecks,
|
||||
) -> ImapResult<Self> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let tls = dc_build_tls(certificate_checks);
|
||||
let tls_stream = 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()
|
||||
.await
|
||||
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
|
||||
|
||||
Ok(Client::Secure(client))
|
||||
}
|
||||
|
||||
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
|
||||
let stream = 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
|
||||
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
|
||||
|
||||
Ok(Client::Insecure(client))
|
||||
}
|
||||
|
||||
pub async fn secure<S: AsRef<str>>(
|
||||
self,
|
||||
domain: S,
|
||||
certificate_checks: CertificateChecks,
|
||||
) -> ImapResult<Client> {
|
||||
match self {
|
||||
Client::Insecure(client) => {
|
||||
let tls = dc_build_tls(certificate_checks);
|
||||
let client_sec = client.secure(domain, tls).await?;
|
||||
|
||||
Ok(Client::Secure(client_sec))
|
||||
}
|
||||
// Nothing to do
|
||||
Client::Secure(_) => Ok(self),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
|
||||
self,
|
||||
auth_type: S,
|
||||
authenticator: &A,
|
||||
) -> Result<Session, (ImapError, Client)> {
|
||||
match self {
|
||||
Client::Secure(i) => match i.authenticate(auth_type, authenticator).await {
|
||||
Ok(session) => Ok(Session::Secure(session)),
|
||||
Err((err, c)) => Err((err, Client::Secure(c))),
|
||||
},
|
||||
Client::Insecure(i) => match i.authenticate(auth_type, authenticator).await {
|
||||
Ok(session) => Ok(Session::Insecure(session)),
|
||||
Err((err, c)) => Err((err, Client::Insecure(c))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
|
||||
self,
|
||||
username: U,
|
||||
password: P,
|
||||
) -> Result<Session, (ImapError, Client)> {
|
||||
match self {
|
||||
Client::Secure(i) => match i.login(username, password).await {
|
||||
Ok(session) => Ok(Session::Secure(session)),
|
||||
Err((err, c)) => Err((err, Client::Secure(c))),
|
||||
},
|
||||
Client::Insecure(i) => match i.login(username, password).await {
|
||||
Ok(session) => Ok(Session::Insecure(session)),
|
||||
Err((err, c)) => Err((err, Client::Insecure(c))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
use super::Imap;
|
||||
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use async_imap::extensions::idle::{Handle as ImapIdleHandle, IdleResponse};
|
||||
use async_native_tls::TlsStream;
|
||||
use async_std::net::TcpStream;
|
||||
use async_std::prelude::*;
|
||||
use async_std::task;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::imap_client::*;
|
||||
|
||||
use super::select_folder;
|
||||
use super::session::Session;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -27,9 +29,6 @@ pub enum Error {
|
||||
#[fail(display = "IMAP select folder error")]
|
||||
SelectFolderError(#[cause] select_folder::Error),
|
||||
|
||||
#[fail(display = "IMAP error")]
|
||||
ImapError(#[cause] async_imap::error::Error),
|
||||
|
||||
#[fail(display = "Setup handle error")]
|
||||
SetupHandleError(#[cause] super::Error),
|
||||
}
|
||||
@@ -40,6 +39,27 @@ impl From<select_folder::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum IdleHandle {
|
||||
Secure(ImapIdleHandle<TlsStream<TcpStream>>),
|
||||
Insecure(ImapIdleHandle<TcpStream>),
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn idle(self) -> IdleHandle {
|
||||
match self {
|
||||
Session::Secure(i) => {
|
||||
let h = i.idle();
|
||||
IdleHandle::Secure(h)
|
||||
}
|
||||
Session::Insecure(i) => {
|
||||
let h = i.idle();
|
||||
IdleHandle::Insecure(h)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Imap {
|
||||
pub fn can_idle(&self) -> bool {
|
||||
task::block_on(async move { self.config.read().await.can_idle })
|
||||
@@ -80,18 +100,21 @@ impl Imap {
|
||||
} else {
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
match idle_wait.await {
|
||||
IdleResponse::NewData(_) => {
|
||||
Ok(IdleResponse::NewData(_)) => {
|
||||
info!(context, "Idle has NewData");
|
||||
}
|
||||
// TODO: idle_wait does not distinguish manual interrupts
|
||||
// from Timeouts if we would know it's a Timeout we could bail
|
||||
// directly and reconnect .
|
||||
IdleResponse::Timeout => {
|
||||
Ok(IdleResponse::Timeout) => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
IdleResponse::ManualInterrupt => {
|
||||
Ok(IdleResponse::ManualInterrupt) => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Idle wait errored: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
// if we can't properly terminate the idle
|
||||
@@ -134,18 +157,21 @@ impl Imap {
|
||||
} else {
|
||||
info!(context, "Idle entering wait-on-remote state");
|
||||
match idle_wait.await {
|
||||
IdleResponse::NewData(_) => {
|
||||
Ok(IdleResponse::NewData(_)) => {
|
||||
info!(context, "Idle has NewData");
|
||||
}
|
||||
// TODO: idle_wait does not distinguish manual interrupts
|
||||
// from Timeouts if we would know it's a Timeout we could bail
|
||||
// directly and reconnect .
|
||||
IdleResponse::Timeout => {
|
||||
Ok(IdleResponse::Timeout) => {
|
||||
info!(context, "Idle-wait timeout or interruption");
|
||||
}
|
||||
IdleResponse::ManualInterrupt => {
|
||||
Ok(IdleResponse::ManualInterrupt) => {
|
||||
info!(context, "Idle wait was interrupted");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Idle wait errored: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
// if we can't properly terminate the idle
|
||||
|
||||
499
src/imap/mod.rs
499
src/imap/mod.rs
@@ -5,6 +5,8 @@
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use async_imap::{
|
||||
error::Result as ImapResult,
|
||||
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
|
||||
@@ -12,11 +14,14 @@ use async_imap::{
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use async_std::task;
|
||||
|
||||
use crate::config::*;
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
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::imap_client::*;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::job::{job_add, Action};
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::message::{self, update_server_uid};
|
||||
@@ -24,10 +29,13 @@ use crate::oauth2::dc_get_oauth2_access_token;
|
||||
use crate::param::Params;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
mod client;
|
||||
mod idle;
|
||||
pub mod select_folder;
|
||||
mod session;
|
||||
|
||||
const DC_IMAP_SEEN: usize = 0x0001;
|
||||
use client::Client;
|
||||
use session::Session;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -63,6 +71,9 @@ pub enum Error {
|
||||
#[fail(display = "IMAP select folder error")]
|
||||
SelectFolderError(#[cause] select_folder::Error),
|
||||
|
||||
#[fail(display = "Mail parse error")]
|
||||
MailParseError(#[cause] mailparse::MailParseError),
|
||||
|
||||
#[fail(display = "No mailbox selected, folder: {:?}", _0)]
|
||||
NoMailbox(String),
|
||||
|
||||
@@ -94,6 +105,12 @@ impl From<select_folder::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mailparse::MailParseError> for Error {
|
||||
fn from(err: mailparse::MailParseError) -> Error {
|
||||
Error::MailParseError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImapActionResult {
|
||||
Failed,
|
||||
@@ -102,7 +119,20 @@ pub enum ImapActionResult {
|
||||
Success,
|
||||
}
|
||||
|
||||
const PREFETCH_FLAGS: &str = "(UID ENVELOPE)";
|
||||
/// Prefetch:
|
||||
/// - Message-ID to check if we already have the message.
|
||||
/// - In-Reply-To and References to check if message is a reply to chat message.
|
||||
/// - Chat-Version to check if a message is a chat message
|
||||
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
|
||||
/// not necessarily sent by Delta Chat.
|
||||
const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
|
||||
MESSAGE-ID \
|
||||
FROM \
|
||||
IN-REPLY-TO REFERENCES \
|
||||
CHAT-VERSION \
|
||||
AUTOCRYPT-SETUP-MESSAGE\
|
||||
)])";
|
||||
const DELETE_CHECK_FLAGS: &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:*";
|
||||
@@ -154,8 +184,10 @@ struct ImapConfig {
|
||||
pub selected_mailbox: Option<Mailbox>,
|
||||
pub selected_folder_needs_expunge: bool,
|
||||
pub can_idle: bool,
|
||||
pub has_xlist: bool,
|
||||
pub imap_delimiter: char,
|
||||
|
||||
/// True if the server has MOVE capability as defined in
|
||||
/// https://tools.ietf.org/html/rfc6851
|
||||
pub can_move: bool,
|
||||
}
|
||||
|
||||
impl Default for ImapConfig {
|
||||
@@ -172,8 +204,7 @@ impl Default for ImapConfig {
|
||||
selected_mailbox: None,
|
||||
selected_folder_needs_expunge: false,
|
||||
can_idle: false,
|
||||
has_xlist: false,
|
||||
imap_delimiter: '.',
|
||||
can_move: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,7 +357,7 @@ impl Imap {
|
||||
cfg.imap_port = 0;
|
||||
|
||||
cfg.can_idle = false;
|
||||
cfg.has_xlist = false;
|
||||
cfg.can_move = false;
|
||||
}
|
||||
|
||||
/// Connects to imap account using already-configured parameters.
|
||||
@@ -387,7 +418,7 @@ impl Imap {
|
||||
true
|
||||
} else {
|
||||
let can_idle = caps.has_str("IDLE");
|
||||
let has_xlist = caps.has_str("XLIST");
|
||||
let can_move = caps.has_str("MOVE");
|
||||
let caps_list = caps.iter().fold(String::new(), |s, c| {
|
||||
if let Capability::Atom(x) = c {
|
||||
s + &format!(" {}", x)
|
||||
@@ -397,7 +428,7 @@ impl Imap {
|
||||
});
|
||||
|
||||
self.config.write().await.can_idle = can_idle;
|
||||
self.config.write().await.has_xlist = has_xlist;
|
||||
self.config.write().await.can_move = can_move;
|
||||
*self.connected.lock().await = true;
|
||||
emit_event!(
|
||||
context,
|
||||
@@ -555,6 +586,9 @@ impl Imap {
|
||||
context: &Context,
|
||||
folder: S,
|
||||
) -> Result<bool> {
|
||||
let show_emails =
|
||||
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default();
|
||||
|
||||
let (uid_validity, last_seen_uid) =
|
||||
self.select_with_uidvalidity(context, folder.as_ref())?;
|
||||
|
||||
@@ -562,7 +596,7 @@ impl Imap {
|
||||
|
||||
let mut list = if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
// fetch messages with larger UID than the last one seen
|
||||
// (`UID FETCH lastseenuid+1:*)`, see RFC 4549
|
||||
// `(UID FETCH lastseenuid+1:*)`, see RFC 4549
|
||||
let set = format!("{}:*", last_seen_uid + 1);
|
||||
match session.uid_fetch(set, PREFETCH_FLAGS).await {
|
||||
Ok(list) => list,
|
||||
@@ -580,11 +614,16 @@ impl Imap {
|
||||
|
||||
list.sort_unstable_by_key(|msg| msg.uid.unwrap_or_default());
|
||||
|
||||
for msg in &list {
|
||||
let cur_uid = msg.uid.unwrap_or_default();
|
||||
for fetch in &list {
|
||||
let cur_uid = fetch.uid.unwrap_or_default();
|
||||
if cur_uid <= last_seen_uid {
|
||||
// seems that at least dovecot sends the last available UID
|
||||
// even if we asked for higher UID+N:*
|
||||
// If the mailbox is not empty, results always include
|
||||
// at least one UID, even if last_seen_uid+1 is past
|
||||
// the last UID in the mailbox. It happens because
|
||||
// uid+1:* is interpreted the same way as *:uid+1.
|
||||
// See https://tools.ietf.org/html/rfc3501#page-61 for
|
||||
// standard reference. Therefore, sometimes we receive
|
||||
// already seen messages and have to filter them out.
|
||||
info!(
|
||||
context,
|
||||
"fetch_new_messages: ignoring uid {}, last seen was {}", cur_uid, last_seen_uid
|
||||
@@ -593,20 +632,9 @@ impl Imap {
|
||||
}
|
||||
read_cnt += 1;
|
||||
|
||||
let message_id = prefetch_get_message_id(msg).unwrap_or_default();
|
||||
|
||||
if !precheck_imf(context, &message_id, folder.as_ref(), cur_uid) {
|
||||
// check passed, go fetch the rest
|
||||
if self.fetch_single_msg(context, &folder, cur_uid).await == 0 {
|
||||
info!(
|
||||
context,
|
||||
"Read error for message {} from \"{}\", trying over later.",
|
||||
message_id,
|
||||
folder.as_ref()
|
||||
);
|
||||
read_errors += 1;
|
||||
}
|
||||
} else {
|
||||
let headers = get_fetch_headers(fetch)?;
|
||||
let message_id = prefetch_get_message_id(&headers).unwrap_or_default();
|
||||
if precheck_imf(context, &message_id, folder.as_ref(), cur_uid) {
|
||||
// we know the message-id already or don't want the message otherwise.
|
||||
info!(
|
||||
context,
|
||||
@@ -614,6 +642,34 @@ impl Imap {
|
||||
message_id,
|
||||
folder.as_ref(),
|
||||
);
|
||||
} else {
|
||||
let show = prefetch_should_download(context, &headers, show_emails)
|
||||
.map_err(|err| {
|
||||
warn!(context, "prefetch_should_download error: {}", err);
|
||||
err
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
if !show {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring new message {} from \"{}\".",
|
||||
message_id,
|
||||
folder.as_ref(),
|
||||
);
|
||||
} else {
|
||||
// check passed, go fetch the rest
|
||||
if let Err(err) = self.fetch_single_msg(context, &folder, cur_uid).await {
|
||||
info!(
|
||||
context,
|
||||
"Read error for message {} from \"{}\", trying over later: {}.",
|
||||
message_id,
|
||||
folder.as_ref(),
|
||||
err
|
||||
);
|
||||
read_errors += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if read_errors == 0 {
|
||||
new_last_seen_uid = cur_uid;
|
||||
@@ -657,17 +713,19 @@ impl Imap {
|
||||
context.sql.set_raw_config(context, &key, Some(&val)).ok();
|
||||
}
|
||||
|
||||
/// Fetches a single message by server UID.
|
||||
///
|
||||
/// If it succeeds, the message should be treated as received even
|
||||
/// if no database entries are created. If the function returns an
|
||||
/// error, the caller should try again later.
|
||||
async fn fetch_single_msg<S: AsRef<str>>(
|
||||
&self,
|
||||
context: &Context,
|
||||
folder: S,
|
||||
server_uid: u32,
|
||||
) -> usize {
|
||||
// the function returns:
|
||||
// 0 the caller should try over again later
|
||||
// or 1 if the messages should be treated as received, the caller should not try to read the message again (even if no database entries are returned)
|
||||
) -> Result<()> {
|
||||
if !self.is_connected().await {
|
||||
return 0;
|
||||
return Err(Error::Other("Not connected".to_string()));
|
||||
}
|
||||
|
||||
let set = format!("{}", server_uid);
|
||||
@@ -686,41 +744,24 @@ impl Imap {
|
||||
folder.as_ref(),
|
||||
err
|
||||
);
|
||||
return 0;
|
||||
return Err(Error::FetchFailed(err));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we could not get a valid imap session, this should be retried
|
||||
self.trigger_reconnect();
|
||||
return 0;
|
||||
return Err(Error::Other("Could not get IMAP session".to_string()));
|
||||
};
|
||||
|
||||
if msgs.is_empty() {
|
||||
warn!(
|
||||
context,
|
||||
"Message #{} does not exist in folder \"{}\".",
|
||||
server_uid,
|
||||
folder.as_ref()
|
||||
);
|
||||
} else {
|
||||
let msg = &msgs[0];
|
||||
|
||||
if let Some(msg) = msgs.first() {
|
||||
// XXX put flags into a set and pass them to dc_receive_imf
|
||||
let is_deleted = msg.flags().any(|flag| match flag {
|
||||
Flag::Deleted => true,
|
||||
_ => false,
|
||||
});
|
||||
let is_seen = msg.flags().any(|flag| match flag {
|
||||
Flag::Seen => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
let flags = if is_seen { DC_IMAP_SEEN } else { 0 };
|
||||
let is_deleted = msg.flags().any(|flag| flag == Flag::Deleted);
|
||||
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
|
||||
|
||||
if !is_deleted && msg.body().is_some() {
|
||||
let body = msg.body().unwrap_or_default();
|
||||
if let Err(err) =
|
||||
dc_receive_imf(context, &body, folder.as_ref(), server_uid, flags as u32)
|
||||
dc_receive_imf(context, &body, folder.as_ref(), server_uid, is_seen)
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
@@ -731,9 +772,20 @@ impl Imap {
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Message #{} does not exist in folder \"{}\".",
|
||||
server_uid,
|
||||
folder.as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
1
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn can_move(&self) -> bool {
|
||||
task::block_on(async move { self.config.read().await.can_move })
|
||||
}
|
||||
|
||||
pub fn mv(
|
||||
@@ -742,7 +794,6 @@ impl Imap {
|
||||
folder: &str,
|
||||
uid: u32,
|
||||
dest_folder: &str,
|
||||
dest_uid: &mut u32,
|
||||
) -> ImapActionResult {
|
||||
task::block_on(async move {
|
||||
if folder == dest_folder {
|
||||
@@ -759,58 +810,76 @@ impl Imap {
|
||||
return imapresult;
|
||||
}
|
||||
// we are connected, and the folder is selected
|
||||
|
||||
// XXX Rust-Imap provides no target uid on mv, so just set it to 0
|
||||
*dest_uid = 0;
|
||||
|
||||
let set = format!("{}", uid);
|
||||
let display_folder_id = format!("{}/{}", folder, uid);
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
match session.uid_mv(&set, &dest_folder).await {
|
||||
Ok(_) => {
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ImapMessageMoved(format!(
|
||||
"IMAP Message {} moved to {}",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
);
|
||||
return ImapActionResult::Success;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
|
||||
folder,
|
||||
uid,
|
||||
dest_folder,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
match session.uid_copy(&set, &dest_folder).await {
|
||||
Ok(_) => {
|
||||
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
|
||||
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
self.config.write().await.selected_folder_needs_expunge = true;
|
||||
ImapActionResult::Success
|
||||
if self.can_move() {
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
match session.uid_mv(&set, &dest_folder).await {
|
||||
Ok(_) => {
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ImapMessageMoved(format!(
|
||||
"IMAP Message {} moved to {}",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
);
|
||||
return ImapActionResult::Success;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
|
||||
folder,
|
||||
uid,
|
||||
dest_folder,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Could not copy message: {}", err);
|
||||
ImapActionResult::Failed
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Server does not support MOVE, fallback to COPY/DELETE {}/{} to {}",
|
||||
folder,
|
||||
uid,
|
||||
dest_folder
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
if let Err(err) = session.uid_copy(&set, &dest_folder).await {
|
||||
warn!(context, "Could not copy message: {}", err);
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
|
||||
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ImapMessageMoved(format!(
|
||||
"IMAP Message {} copied to {} (delete FAILED)",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
);
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
self.config.write().await.selected_folder_needs_expunge = true;
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ImapMessageMoved(format!(
|
||||
"IMAP Message {} copied to {} (delete successfull)",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
);
|
||||
ImapActionResult::Success
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -915,16 +984,15 @@ impl Imap {
|
||||
})
|
||||
}
|
||||
|
||||
// only returns 0 on connection problems; we should try later again in this case *
|
||||
pub fn delete_msg(
|
||||
&self,
|
||||
context: &Context,
|
||||
message_id: &str,
|
||||
folder: &str,
|
||||
uid: &mut u32,
|
||||
uid: u32,
|
||||
) -> ImapActionResult {
|
||||
task::block_on(async move {
|
||||
if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, *uid) {
|
||||
if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, uid) {
|
||||
return imapresult;
|
||||
}
|
||||
// we are connected, and the folder is selected
|
||||
@@ -935,19 +1003,23 @@ impl Imap {
|
||||
// double-check that we are deleting the correct message-id
|
||||
// this comes at the expense of another imap query
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
match session.uid_fetch(set, PREFETCH_FLAGS).await {
|
||||
match session.uid_fetch(set, DELETE_CHECK_FLAGS).await {
|
||||
Ok(msgs) => {
|
||||
if msgs.is_empty() {
|
||||
let fetch = if let Some(fetch) = msgs.first() {
|
||||
fetch
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot delete on IMAP, {}: imap entry gone '{}'",
|
||||
display_imap_id,
|
||||
message_id,
|
||||
);
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
let remote_message_id =
|
||||
prefetch_get_message_id(msgs.first().unwrap()).unwrap_or_default();
|
||||
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!(
|
||||
@@ -957,7 +1029,7 @@ impl Imap {
|
||||
remote_message_id,
|
||||
message_id,
|
||||
);
|
||||
*uid = 0;
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -965,18 +1037,18 @@ impl Imap {
|
||||
context,
|
||||
"Cannot delete {} on IMAP: {}", display_imap_id, err
|
||||
);
|
||||
*uid = 0;
|
||||
return ImapActionResult::RetryLater;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mark the message for deletion
|
||||
if !self.add_flag_finalized(context, *uid, "\\Deleted").await {
|
||||
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot mark message {} as \"Deleted\".", display_imap_id
|
||||
);
|
||||
ImapActionResult::Failed
|
||||
ImapActionResult::RetryLater
|
||||
} else {
|
||||
emit_event!(
|
||||
context,
|
||||
@@ -995,12 +1067,14 @@ impl Imap {
|
||||
let folders_configured = context
|
||||
.sql
|
||||
.get_raw_config_int(context, "folders_configured");
|
||||
if folders_configured.unwrap_or_default() >= 3 {
|
||||
// the "3" here we increase if we have future updates to
|
||||
// to folder configuration
|
||||
if folders_configured.unwrap_or_default() >= DC_FOLDERS_CONFIGURED_VERSION {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.configure_folders(context, create_mvbox)
|
||||
}
|
||||
|
||||
pub fn configure_folders(&self, context: &Context, create_mvbox: bool) -> Result<()> {
|
||||
task::block_on(async move {
|
||||
if !self.is_connected().await {
|
||||
return Err(Error::NoConnection);
|
||||
@@ -1025,7 +1099,15 @@ impl Imap {
|
||||
});
|
||||
info!(context, "sentbox folder is {:?}", sentbox_folder);
|
||||
|
||||
let delimiter = self.config.read().await.imap_delimiter;
|
||||
let mut delimiter = ".";
|
||||
if let Some(folder) = folders.first() {
|
||||
if let Some(d) = folder.delimiter() {
|
||||
if !d.is_empty() {
|
||||
delimiter = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
|
||||
let fallback_folder = format!("INBOX{}DeltaChat", delimiter);
|
||||
|
||||
let mut mvbox_folder = folders
|
||||
@@ -1089,9 +1171,11 @@ impl Imap {
|
||||
Some(sentbox_folder.name()),
|
||||
)?;
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int(context, "folders_configured", 3)?;
|
||||
context.sql.set_raw_config_int(
|
||||
context,
|
||||
"folders_configured",
|
||||
DC_FOLDERS_CONFIGURED_VERSION,
|
||||
)?;
|
||||
}
|
||||
info!(context, "FINISHED configuring IMAP-folders.");
|
||||
Ok(())
|
||||
@@ -1099,7 +1183,6 @@ impl Imap {
|
||||
}
|
||||
|
||||
async fn list_folders(&self, session: &mut Session, context: &Context) -> Option<Vec<Name>> {
|
||||
// TODO: use xlist when available
|
||||
match session.list(Some(""), Some("*")).await {
|
||||
Ok(list) => {
|
||||
if list.is_empty() {
|
||||
@@ -1127,13 +1210,13 @@ impl Imap {
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.setup_handle_if_needed(context).await {
|
||||
error!(context, "could not setup imap connection: {:?}", err);
|
||||
error!(context, "could not setup imap connection: {}", err);
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.select_folder(context, Some(&folder)).await {
|
||||
error!(
|
||||
context,
|
||||
"Could not select {} for expunging: {:?}", folder, err
|
||||
"Could not select {} for expunging: {}", folder, err
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1153,9 +1236,21 @@ impl Imap {
|
||||
emit_event!(context, Event::ImapFolderEmptied(folder.to_string()));
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "expunge failed {}: {:?}", folder, err);
|
||||
error!(context, "expunge failed {}: {}", folder, err);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = crate::sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs SET server_folder='',server_uid=0 WHERE server_folder=?",
|
||||
params![folder],
|
||||
) {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to reset server_uid and server_folder for deleted messages: {}", err
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1206,17 +1301,52 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
|
||||
message::rfc724_mid_exists(context, &rfc724_mid)
|
||||
{
|
||||
if old_server_folder.is_empty() && old_server_uid == 0 {
|
||||
info!(context, "[move] detected bbc-self {}", rfc724_mid,);
|
||||
context.do_heuristics_moves(server_folder.as_ref(), msg_id);
|
||||
job_add(
|
||||
info!(
|
||||
context,
|
||||
Action::MarkseenMsgOnImap,
|
||||
msg_id.to_u32() as i32,
|
||||
Params::new(),
|
||||
0,
|
||||
"[move] detected bcc-self {} as {}/{}", rfc724_mid, server_folder, server_uid
|
||||
);
|
||||
|
||||
let delete_server_after = context.get_config_delete_server_after();
|
||||
|
||||
if delete_server_after != Some(0) {
|
||||
context.do_heuristics_moves(server_folder.as_ref(), msg_id);
|
||||
job_add(
|
||||
context,
|
||||
Action::MarkseenMsgOnImap,
|
||||
msg_id.to_u32() as i32,
|
||||
Params::new(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
} else if old_server_folder != server_folder {
|
||||
info!(context, "[move] detected moved message {}", rfc724_mid,);
|
||||
info!(
|
||||
context,
|
||||
"[move] detected message {} moved by other device from {}/{} to {}/{}",
|
||||
rfc724_mid,
|
||||
old_server_folder,
|
||||
old_server_uid,
|
||||
server_folder,
|
||||
server_uid
|
||||
);
|
||||
} else if old_server_uid == 0 {
|
||||
info!(
|
||||
context,
|
||||
"[move] detected message {} moved by us from {}/{} to {}/{}",
|
||||
rfc724_mid,
|
||||
old_server_folder,
|
||||
old_server_uid,
|
||||
server_folder,
|
||||
server_uid
|
||||
);
|
||||
} else if old_server_uid != server_uid {
|
||||
warn!(
|
||||
context,
|
||||
"UID for message {} in folder {} changed from {} to {}",
|
||||
rfc724_mid,
|
||||
server_folder,
|
||||
old_server_uid,
|
||||
server_uid
|
||||
);
|
||||
}
|
||||
|
||||
if old_server_folder != server_folder || old_server_uid != server_uid {
|
||||
@@ -1228,46 +1358,67 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_message_id(message_id: &[u8]) -> crate::error::Result<String> {
|
||||
let value = std::str::from_utf8(message_id)?;
|
||||
let addrs = mailparse::addrparse(value)
|
||||
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
|
||||
|
||||
if let Some(info) = addrs.extract_single_info() {
|
||||
return Ok(info.addr);
|
||||
}
|
||||
|
||||
bail!("could not parse message_id: {}", value);
|
||||
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
|
||||
let header_bytes = match prefetch_msg.header() {
|
||||
Some(header_bytes) => header_bytes,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
let (headers, _) = mailparse::parse_headers(header_bytes)?;
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
|
||||
if prefetch_msg.envelope().is_none() {
|
||||
return Err(Error::Other(
|
||||
"prefectch: message has no envelope".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let message_id = prefetch_msg.envelope().unwrap().message_id;
|
||||
if message_id.is_none() {
|
||||
return Err(Error::Other("prefetch: No message ID found".to_string()));
|
||||
}
|
||||
|
||||
parse_message_id(&message_id.unwrap()).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_message_id() {
|
||||
assert_eq!(
|
||||
parse_message_id(b"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
|
||||
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_message_id(b"<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
|
||||
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
|
||||
);
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
fn prefetch_is_reply_to_chat_message(context: &Context, headers: &[mailparse::MailHeader]) -> bool {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo) {
|
||||
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::References) {
|
||||
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn prefetch_should_download(
|
||||
context: &Context,
|
||||
headers: &[mailparse::MailHeader],
|
||||
show_emails: ShowEmails,
|
||||
) -> Result<bool> {
|
||||
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);
|
||||
|
||||
// Autocrypt Setup Message should be shown even if it is from non-chat client.
|
||||
let is_autocrypt_setup_message = headers
|
||||
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
||||
.is_some();
|
||||
|
||||
let from_field = headers
|
||||
.get_header_value(HeaderDef::From_)
|
||||
.unwrap_or_default();
|
||||
|
||||
let (_contact_id, blocked_contact, origin) = from_field_to_contact_id(context, &from_field)?;
|
||||
let accepted_contact = origin.is_known();
|
||||
|
||||
let show = is_autocrypt_setup_message
|
||||
|| match show_emails {
|
||||
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
|
||||
ShowEmails::AcceptedContacts => {
|
||||
is_chat_message || is_reply_to_chat_message || accepted_contact
|
||||
}
|
||||
ShowEmails::All => true,
|
||||
};
|
||||
let show = show && !blocked_contact;
|
||||
Ok(show)
|
||||
}
|
||||
|
||||
@@ -1,125 +1,18 @@
|
||||
use async_imap::{
|
||||
error::{Error as ImapError, Result as ImapResult},
|
||||
extensions::idle::Handle as ImapIdleHandle,
|
||||
error::Result as ImapResult,
|
||||
types::{Capabilities, Fetch, Mailbox, Name},
|
||||
Client as ImapClient, Session as ImapSession,
|
||||
Session as ImapSession,
|
||||
};
|
||||
use async_native_tls::TlsStream;
|
||||
use async_std::net::{self, TcpStream};
|
||||
use async_std::net::TcpStream;
|
||||
use async_std::prelude::*;
|
||||
|
||||
use crate::login_param::{dc_build_tls, CertificateChecks};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Client {
|
||||
Secure(ImapClient<TlsStream<TcpStream>>),
|
||||
Insecure(ImapClient<TcpStream>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Session {
|
||||
Secure(ImapSession<TlsStream<TcpStream>>),
|
||||
Insecure(ImapSession<TcpStream>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum IdleHandle {
|
||||
Secure(ImapIdleHandle<TlsStream<TcpStream>>),
|
||||
Insecure(ImapIdleHandle<TcpStream>),
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
|
||||
addr: A,
|
||||
domain: S,
|
||||
certificate_checks: CertificateChecks,
|
||||
) -> ImapResult<Self> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let tls = dc_build_tls(certificate_checks)?;
|
||||
let tls_connector: async_native_tls::TlsConnector = tls.into();
|
||||
let tls_stream = tls_connector.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()
|
||||
.await
|
||||
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
|
||||
|
||||
Ok(Client::Secure(client))
|
||||
}
|
||||
|
||||
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
|
||||
let stream = 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
|
||||
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
|
||||
|
||||
Ok(Client::Insecure(client))
|
||||
}
|
||||
|
||||
pub async fn secure<S: AsRef<str>>(
|
||||
self,
|
||||
domain: S,
|
||||
certificate_checks: CertificateChecks,
|
||||
) -> ImapResult<Client> {
|
||||
match self {
|
||||
Client::Insecure(client) => {
|
||||
let tls = dc_build_tls(certificate_checks)?;
|
||||
let tls_stream = tls.into();
|
||||
|
||||
let client_sec = client.secure(domain, &tls_stream).await?;
|
||||
|
||||
Ok(Client::Secure(client_sec))
|
||||
}
|
||||
// Nothing to do
|
||||
Client::Secure(_) => Ok(self),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
|
||||
self,
|
||||
auth_type: S,
|
||||
authenticator: &A,
|
||||
) -> Result<Session, (ImapError, Client)> {
|
||||
match self {
|
||||
Client::Secure(i) => match i.authenticate(auth_type, authenticator).await {
|
||||
Ok(session) => Ok(Session::Secure(session)),
|
||||
Err((err, c)) => Err((err, Client::Secure(c))),
|
||||
},
|
||||
Client::Insecure(i) => match i.authenticate(auth_type, authenticator).await {
|
||||
Ok(session) => Ok(Session::Insecure(session)),
|
||||
Err((err, c)) => Err((err, Client::Insecure(c))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
|
||||
self,
|
||||
username: U,
|
||||
password: P,
|
||||
) -> Result<Session, (ImapError, Client)> {
|
||||
match self {
|
||||
Client::Secure(i) => match i.login(username, password).await {
|
||||
Ok(session) => Ok(Session::Secure(session)),
|
||||
Err((err, c)) => Err((err, Client::Secure(c))),
|
||||
},
|
||||
Client::Insecure(i) => match i.login(username, password).await {
|
||||
Ok(session) => Ok(Session::Insecure(session)),
|
||||
Err((err, c)) => Err((err, Client::Insecure(c))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub async fn capabilities(&mut self) -> ImapResult<Capabilities> {
|
||||
let res = match self {
|
||||
@@ -230,19 +123,6 @@ impl Session {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn idle(self) -> IdleHandle {
|
||||
match self {
|
||||
Session::Secure(i) => {
|
||||
let h = i.idle();
|
||||
IdleHandle::Secure(h)
|
||||
}
|
||||
Session::Insecure(i) => {
|
||||
let h = i.idle();
|
||||
IdleHandle::Insecure(h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn uid_store<S1, S2>(&mut self, uid_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
|
||||
where
|
||||
S1: AsRef<str>,
|
||||
53
src/imex.rs
53
src/imex.rs
@@ -10,7 +10,6 @@ use crate::blob::BlobObject;
|
||||
use crate::chat;
|
||||
use crate::chat::delete_and_reset_all_device_msgs;
|
||||
use crate::config::Config;
|
||||
use crate::configure::*;
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
@@ -18,7 +17,7 @@ use crate::e2ee;
|
||||
use crate::error::*;
|
||||
use crate::events::Event;
|
||||
use crate::job::*;
|
||||
use crate::key::*;
|
||||
use crate::key::{self, Key};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::*;
|
||||
@@ -289,7 +288,6 @@ fn set_self_key(
|
||||
.and_then(|(k, h)| k.split_key().map(|pub_key| (k, pub_key, h)));
|
||||
|
||||
ensure!(keys.is_some(), "Not a valid private key");
|
||||
|
||||
let (private_key, public_key, header) = keys.unwrap();
|
||||
let preferencrypt = header.get("Autocrypt-Prefer-Encrypt");
|
||||
match preferencrypt.map(|s| s.as_str()) {
|
||||
@@ -314,34 +312,26 @@ fn set_self_key(
|
||||
|
||||
let self_addr = context.get_config(Config::ConfiguredAddr);
|
||||
ensure!(self_addr.is_some(), "Missing self addr");
|
||||
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
|
||||
|
||||
// XXX maybe better make dc_key_save_self_keypair delete things
|
||||
sql::execute(
|
||||
let (public, secret) = match (public_key, private_key) {
|
||||
(Key::Public(p), Key::Secret(s)) => (p, s),
|
||||
_ => bail!("wrong keys unpacked"),
|
||||
};
|
||||
let keypair = pgp::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
secret,
|
||||
};
|
||||
key::store_self_keypair(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
|
||||
params![public_key.to_bytes(), private_key.to_bytes()],
|
||||
&keypair,
|
||||
if set_default {
|
||||
key::KeyPairUse::Default
|
||||
} else {
|
||||
key::KeyPairUse::ReadOnly
|
||||
},
|
||||
)?;
|
||||
|
||||
if set_default {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE keypairs SET is_default=0;",
|
||||
params![],
|
||||
)?;
|
||||
}
|
||||
|
||||
if !dc_key_save_self_keypair(
|
||||
context,
|
||||
&public_key,
|
||||
&private_key,
|
||||
self_addr.unwrap_or_default(),
|
||||
set_default,
|
||||
&context.sql,
|
||||
) {
|
||||
bail!("Cannot save keypair, internal key-state possibly corrupted now!");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -423,7 +413,7 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
|
||||
);
|
||||
|
||||
ensure!(
|
||||
!dc_is_configured(context),
|
||||
!context.is_configured(),
|
||||
"Cannot import backups to accounts in use."
|
||||
);
|
||||
context.sql.close(&context);
|
||||
@@ -746,7 +736,6 @@ fn export_key_to_asc_file(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
|
||||
use crate::test_utils::*;
|
||||
use ::pgp::armor::BlockType;
|
||||
@@ -767,7 +756,6 @@ mod tests {
|
||||
assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
|
||||
assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
|
||||
assert!(msg.contains("Passphrase-Begin: he\n"));
|
||||
assert!(msg.contains("==\n"));
|
||||
assert!(msg.contains("-----END PGP MESSAGE-----\n"));
|
||||
}
|
||||
|
||||
@@ -801,8 +789,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_export_key_to_asc_file() {
|
||||
let context = dummy_context();
|
||||
let base64 = include_str!("../test-data/key/public.asc");
|
||||
let key = Key::from_base64(base64, KeyType::Public).unwrap();
|
||||
let key = Key::from(alice_keypair().public);
|
||||
let blobdir = "$BLOBDIR";
|
||||
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key).is_ok());
|
||||
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
|
||||
|
||||
352
src/job.rs
352
src/job.rs
@@ -80,10 +80,14 @@ pub enum Action {
|
||||
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
|
||||
Housekeeping = 105, // low priority ...
|
||||
EmptyServer = 107,
|
||||
DeleteMsgOnImap = 110,
|
||||
MarkseenMdnOnImap = 120,
|
||||
OldDeleteMsgOnImap = 110,
|
||||
MarkseenMsgOnImap = 130,
|
||||
|
||||
// Moving message is prioritized lower than deletion so we don't
|
||||
// bother moving message if it is already scheduled for deletion.
|
||||
MoveMsg = 200,
|
||||
|
||||
DeleteMsgOnImap = 210,
|
||||
ConfigureImap = 900,
|
||||
ImexImap = 910, // ... high priority
|
||||
|
||||
@@ -108,9 +112,9 @@ impl From<Action> for Thread {
|
||||
Unknown => Thread::Unknown,
|
||||
|
||||
Housekeeping => Thread::Imap,
|
||||
OldDeleteMsgOnImap => Thread::Imap,
|
||||
DeleteMsgOnImap => Thread::Imap,
|
||||
EmptyServer => Thread::Imap,
|
||||
MarkseenMdnOnImap => Thread::Imap,
|
||||
MarkseenMsgOnImap => Thread::Imap,
|
||||
MoveMsg => Thread::Imap,
|
||||
ConfigureImap => Thread::Imap,
|
||||
@@ -193,9 +197,31 @@ impl Job {
|
||||
Err(crate::smtp::send::Error::SendError(err)) => {
|
||||
// Remote error, retry later.
|
||||
warn!(context, "SMTP failed to send: {}", err);
|
||||
smtp.disconnect();
|
||||
self.pending_error = Some(err.to_string());
|
||||
Status::RetryLater
|
||||
|
||||
let res = match err {
|
||||
async_smtp::smtp::error::Error::Permanent(_) => {
|
||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||
}
|
||||
async_smtp::smtp::error::Error::Transient(_) => {
|
||||
// We got a transient 4xx response from SMTP server.
|
||||
// Give some time until the server-side error maybe goes away.
|
||||
Status::RetryLater
|
||||
}
|
||||
_ => {
|
||||
if smtp.has_maybe_stale_connection() {
|
||||
info!(context, "stale connection? immediately reconnecting");
|
||||
Status::RetryNow
|
||||
} else {
|
||||
Status::RetryLater
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// this clears last_success info
|
||||
smtp.disconnect();
|
||||
|
||||
res
|
||||
}
|
||||
Err(crate::smtp::send::Error::EnvelopeError(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
@@ -369,7 +395,7 @@ impl Job {
|
||||
}
|
||||
|
||||
self.smtp_send(context, recipients, body, self.job_id, || {
|
||||
// Remove additional SendMdn jobs we have aggretated into this one.
|
||||
// Remove additional SendMdn jobs we have aggregated into this one.
|
||||
job_kill_ids(context, &additional_job_ids)?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -391,18 +417,12 @@ impl Job {
|
||||
|
||||
if let Some(dest_folder) = dest_folder {
|
||||
let server_folder = msg.server_folder.as_ref().unwrap();
|
||||
let mut dest_uid = 0;
|
||||
|
||||
match imap_inbox.mv(
|
||||
context,
|
||||
server_folder,
|
||||
msg.server_uid,
|
||||
&dest_folder,
|
||||
&mut dest_uid,
|
||||
) {
|
||||
match imap_inbox.mv(context, server_folder, msg.server_uid, &dest_folder) {
|
||||
ImapActionResult::RetryLater => Status::RetryLater,
|
||||
ImapActionResult::Success => {
|
||||
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, dest_uid);
|
||||
// XXX Rust-Imap provides no target uid on mv, so just set it to 0
|
||||
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, 0);
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
ImapActionResult::Failed => {
|
||||
@@ -415,14 +435,29 @@ impl Job {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a message on the server.
|
||||
///
|
||||
/// foreign_id is a MsgId pointing to a message in the trash chat
|
||||
/// or a hidden message.
|
||||
///
|
||||
/// This job removes the database record. If there are no more
|
||||
/// records pointing to the same message on the server, the job
|
||||
/// also removes the message on the server.
|
||||
#[allow(non_snake_case)]
|
||||
fn DeleteMsgOnImap(&mut self, context: &Context) -> Status {
|
||||
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
|
||||
|
||||
let mut msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)));
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)));
|
||||
|
||||
if !msg.rfc724_mid.is_empty() {
|
||||
if message::rfc724_mid_cnt(context, &msg.rfc724_mid) > 1 {
|
||||
let cnt = message::rfc724_mid_cnt(context, &msg.rfc724_mid);
|
||||
info!(
|
||||
context,
|
||||
"Running delete job for message {} which has {} entries in the database",
|
||||
&msg.rfc724_mid,
|
||||
cnt
|
||||
);
|
||||
if cnt > 1 {
|
||||
info!(
|
||||
context,
|
||||
"The message is deleted from the server when all parts are deleted.",
|
||||
@@ -432,13 +467,47 @@ impl Job {
|
||||
we delete the message from the server */
|
||||
let mid = msg.rfc724_mid;
|
||||
let server_folder = msg.server_folder.as_ref().unwrap();
|
||||
let res = imap_inbox.delete_msg(context, &mid, server_folder, &mut msg.server_uid);
|
||||
if res == ImapActionResult::RetryLater {
|
||||
// XXX RetryLater is converted to RetryNow here
|
||||
return Status::RetryNow;
|
||||
let res = if msg.server_uid == 0 {
|
||||
// Message is already deleted on IMAP server.
|
||||
ImapActionResult::AlreadyDone
|
||||
} else {
|
||||
imap_inbox.delete_msg(context, &mid, server_folder, msg.server_uid)
|
||||
};
|
||||
match res {
|
||||
ImapActionResult::AlreadyDone | ImapActionResult::Success => {}
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
// If job has failed, for example due to some
|
||||
// IMAP bug, we postpone it instead of failing
|
||||
// immediately. This will prevent adding it
|
||||
// immediately again if user has enabled
|
||||
// automatic message deletion. Without this,
|
||||
// we might waste a lot of traffic constantly
|
||||
// retrying message deletion.
|
||||
return Status::RetryLater;
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::delete_from_db(context, msg.id);
|
||||
if msg.chat_id.is_trash() || msg.hidden {
|
||||
// Messages are stored in trash chat only to keep
|
||||
// their server UID and Message-ID. Once message is
|
||||
// deleted from the server, database record can be
|
||||
// removed as well.
|
||||
//
|
||||
// Hidden messages are similar to trashed, but are
|
||||
// related to some chat. We also delete their
|
||||
// database records.
|
||||
job_try!(msg.id.delete_from_db(context))
|
||||
} else {
|
||||
// Remove server UID from the database record.
|
||||
//
|
||||
// We have either just removed the message from the
|
||||
// server, in which case UID is not valid anymore, or
|
||||
// we have more refernces to the same server UID, so
|
||||
// we remove UID to reduce the number of messages
|
||||
// pointing to the corresponding UID. Once the counter
|
||||
// reaches zero, we will remove the message.
|
||||
job_try!(msg.id.unlink(context));
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
} else {
|
||||
/* eg. device messages have no Message-ID */
|
||||
@@ -490,43 +559,6 @@ impl Job {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn MarkseenMdnOnImap(&mut self, context: &Context) -> Status {
|
||||
let folder = self
|
||||
.param
|
||||
.get(Param::ServerFolder)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let uid = self.param.get_int(Param::ServerUid).unwrap_or_default() as u32;
|
||||
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
|
||||
if imap_inbox.set_seen(context, &folder, uid) == ImapActionResult::RetryLater {
|
||||
return Status::RetryLater;
|
||||
}
|
||||
if self.param.get_bool(Param::AlsoMove).unwrap_or_default() {
|
||||
if let Err(err) = imap_inbox.ensure_configured_folders(context, true) {
|
||||
warn!(context, "configuring folders failed: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
let dest_folder = context
|
||||
.sql
|
||||
.get_raw_config(context, "configured_mvbox_folder");
|
||||
if let Some(dest_folder) = dest_folder {
|
||||
let mut dest_uid = 0;
|
||||
if ImapActionResult::RetryLater
|
||||
== imap_inbox.mv(context, &folder, uid, &dest_folder, &mut dest_uid)
|
||||
{
|
||||
Status::RetryLater
|
||||
} else {
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
} else {
|
||||
Status::Finished(Err(format_err!("MVBOX is not configured")))
|
||||
}
|
||||
} else {
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* delete all pending jobs with the given action */
|
||||
@@ -792,7 +824,36 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
|
||||
};
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar)?;
|
||||
let mut rendered_msg = mimefactory.render().map_err(|err| {
|
||||
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
let from = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.unwrap_or_default();
|
||||
let lowercase_from = from.to_lowercase();
|
||||
|
||||
// Send BCC to self if it is enabled and we are not going to
|
||||
// delete it immediately.
|
||||
if context.get_config_bool(Config::BccSelf)
|
||||
&& context.get_config_delete_server_after() != Some(0)
|
||||
&& !recipients
|
||||
.iter()
|
||||
.any(|x| x.to_lowercase() == lowercase_from)
|
||||
{
|
||||
recipients.push(from);
|
||||
}
|
||||
|
||||
if recipients.is_empty() {
|
||||
// may happen eg. for groups with only SELF and bcc_self disabled
|
||||
info!(
|
||||
context,
|
||||
"message {} has no recipient, skipping smtp-send", msg_id
|
||||
);
|
||||
set_delivered(context, msg_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rendered_msg = mimefactory.render().map_err(|err| {
|
||||
message::set_msg_failed(context, msg_id, Some(err.to_string()));
|
||||
err
|
||||
})?;
|
||||
@@ -811,26 +872,6 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
let lowercase_from = rendered_msg.from.to_lowercase();
|
||||
if context.get_config_bool(Config::BccSelf)
|
||||
&& !rendered_msg
|
||||
.recipients
|
||||
.iter()
|
||||
.any(|x| x.to_lowercase() == lowercase_from)
|
||||
{
|
||||
rendered_msg.recipients.push(rendered_msg.from.clone());
|
||||
}
|
||||
|
||||
if rendered_msg.recipients.is_empty() {
|
||||
// may happen eg. for groups with only SELF and bcc_self disabled
|
||||
info!(
|
||||
context,
|
||||
"message {} has no recipient, skipping smtp-send", msg_id
|
||||
);
|
||||
set_delivered(context, msg_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if rendered_msg.is_gossiped {
|
||||
chat::set_gossiped_timestamp(context, msg.chat_id, time())?;
|
||||
}
|
||||
@@ -849,7 +890,7 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
|
||||
}
|
||||
|
||||
if attach_selfavatar {
|
||||
if let Err(err) = chat::set_selfavatar_timestamp(context, msg.chat_id, time()) {
|
||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()) {
|
||||
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
|
||||
}
|
||||
}
|
||||
@@ -859,7 +900,48 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
|
||||
msg.save_param_to_disk(context);
|
||||
}
|
||||
|
||||
add_smtp_job(context, Action::SendMsgToSmtp, msg.id, &rendered_msg)?;
|
||||
add_smtp_job(
|
||||
context,
|
||||
Action::SendMsgToSmtp,
|
||||
msg.id,
|
||||
recipients,
|
||||
&rendered_msg,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_imap_deletion_jobs(context: &Context) -> sql::Result<()> {
|
||||
if let Some(delete_server_after) = context.get_config_delete_server_after() {
|
||||
let threshold_timestamp = time() - delete_server_after;
|
||||
|
||||
// Select all expired messages which don't have a
|
||||
// corresponding message deletion job yet.
|
||||
let msg_ids = context.sql.query_map(
|
||||
"SELECT id FROM msgs \
|
||||
WHERE timestamp < ? \
|
||||
AND server_uid != 0 \
|
||||
AND NOT EXISTS (SELECT 1 FROM jobs WHERE foreign_id = msgs.id \
|
||||
AND action = ?)",
|
||||
params![threshold_timestamp, Action::DeleteMsgOnImap],
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)?;
|
||||
|
||||
// Schedule IMAP deletion for expired messages.
|
||||
for msg_id in msg_ids {
|
||||
job_add(
|
||||
context,
|
||||
Action::DeleteMsgOnImap,
|
||||
msg_id.to_u32() as i32,
|
||||
Params::new(),
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -871,6 +953,9 @@ pub fn perform_inbox_jobs(context: &Context) {
|
||||
*context.probe_imap_network.write().unwrap() = false;
|
||||
*context.perform_inbox_jobs_needed.write().unwrap() = false;
|
||||
|
||||
if let Err(err) = add_imap_deletion_jobs(context) {
|
||||
warn!(context, "Can't add IMAP message deletion jobs: {}", err);
|
||||
}
|
||||
job_perform(context, Thread::Imap, probe_imap_network);
|
||||
info!(context, "dc_perform_inbox_jobs ended.",);
|
||||
}
|
||||
@@ -889,8 +974,8 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
|
||||
|
||||
// some configuration jobs are "exclusive":
|
||||
// - they are always executed in the imap-thread and the smtp-thread is suspended during execution
|
||||
// - they may change the database handle change the database handle; we do not keep old pointers therefore
|
||||
// - they can be re-executed one time AT_ONCE, but they are not save in the database for later execution
|
||||
// - they may change the database handle; we do not keep old pointers therefore
|
||||
// - they can be re-executed one time AT_ONCE, but they are not saved in the database for later execution
|
||||
if Action::ConfigureImap == job.action || Action::ImexImap == job.action {
|
||||
job_kill_action(context, job.action);
|
||||
context
|
||||
@@ -908,52 +993,10 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
|
||||
suspend_smtp_thread(context, true);
|
||||
}
|
||||
|
||||
let try_res = (0..2)
|
||||
.map(|tries| {
|
||||
info!(
|
||||
context,
|
||||
"{} performs immediate try {} of job {}", thread, tries, job
|
||||
);
|
||||
|
||||
let try_res = match job.action {
|
||||
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
|
||||
Action::SendMsgToSmtp => job.SendMsgToSmtp(context),
|
||||
Action::EmptyServer => job.EmptyServer(context),
|
||||
Action::DeleteMsgOnImap => job.DeleteMsgOnImap(context),
|
||||
Action::MarkseenMsgOnImap => job.MarkseenMsgOnImap(context),
|
||||
Action::MarkseenMdnOnImap => job.MarkseenMdnOnImap(context),
|
||||
Action::MoveMsg => job.MoveMsg(context),
|
||||
Action::SendMdn => job.SendMdn(context),
|
||||
Action::ConfigureImap => JobConfigureImap(context),
|
||||
Action::ImexImap => match JobImexImap(context, &job) {
|
||||
Ok(()) => Status::Finished(Ok(())),
|
||||
Err(err) => {
|
||||
error!(context, "{}", err);
|
||||
Status::Finished(Err(err))
|
||||
}
|
||||
},
|
||||
Action::MaybeSendLocations => location::JobMaybeSendLocations(context, &job),
|
||||
Action::MaybeSendLocationsEnded => {
|
||||
location::JobMaybeSendLocationsEnded(context, &mut job)
|
||||
}
|
||||
Action::Housekeeping => {
|
||||
sql::housekeeping(context);
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
context,
|
||||
"{} finished immediate try {} of job {}", thread, tries, job
|
||||
);
|
||||
|
||||
try_res
|
||||
})
|
||||
.find(|try_res| match try_res {
|
||||
Status::RetryNow => false,
|
||||
_ => true,
|
||||
})
|
||||
.unwrap_or(Status::RetryNow);
|
||||
let try_res = match perform_job_action(context, &mut job, thread, 0) {
|
||||
Status::RetryNow => perform_job_action(context, &mut job, thread, 1),
|
||||
x => x,
|
||||
};
|
||||
|
||||
if Action::ConfigureImap == job.action || Action::ImexImap == job.action {
|
||||
context
|
||||
@@ -1044,6 +1087,45 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_job_action(context: &Context, mut job: &mut Job, thread: Thread, tries: u32) -> Status {
|
||||
info!(
|
||||
context,
|
||||
"{} begin immediate try {} of job {}", thread, tries, job
|
||||
);
|
||||
|
||||
let try_res = match job.action {
|
||||
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
|
||||
Action::SendMsgToSmtp => job.SendMsgToSmtp(context),
|
||||
Action::EmptyServer => job.EmptyServer(context),
|
||||
Action::OldDeleteMsgOnImap => job.DeleteMsgOnImap(context),
|
||||
Action::DeleteMsgOnImap => job.DeleteMsgOnImap(context),
|
||||
Action::MarkseenMsgOnImap => job.MarkseenMsgOnImap(context),
|
||||
Action::MoveMsg => job.MoveMsg(context),
|
||||
Action::SendMdn => job.SendMdn(context),
|
||||
Action::ConfigureImap => JobConfigureImap(context),
|
||||
Action::ImexImap => match JobImexImap(context, &job) {
|
||||
Ok(()) => Status::Finished(Ok(())),
|
||||
Err(err) => {
|
||||
error!(context, "{}", err);
|
||||
Status::Finished(Err(err))
|
||||
}
|
||||
},
|
||||
Action::MaybeSendLocations => location::JobMaybeSendLocations(context, &job),
|
||||
Action::MaybeSendLocationsEnded => location::JobMaybeSendLocationsEnded(context, &mut job),
|
||||
Action::Housekeeping => {
|
||||
sql::housekeeping(context);
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
context,
|
||||
"{} finished immediate try {} of job {}", thread, tries, job
|
||||
);
|
||||
|
||||
try_res
|
||||
}
|
||||
|
||||
fn get_backoff_time_offset(tries: u32) -> i64 {
|
||||
let n = 2_i32.pow(tries - 1) * 60;
|
||||
let mut rng = thread_rng();
|
||||
@@ -1080,17 +1162,15 @@ fn add_smtp_job(
|
||||
context: &Context,
|
||||
action: Action,
|
||||
msg_id: MsgId,
|
||||
recipients: Vec<String>,
|
||||
rendered_msg: &RenderedEmail,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!rendered_msg.recipients.is_empty(),
|
||||
"no recipients for smtp job set"
|
||||
);
|
||||
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
|
||||
let mut param = Params::new();
|
||||
let bytes = &rendered_msg.message;
|
||||
let blob = BlobObject::create(context, &rendered_msg.rfc724_mid, bytes)?;
|
||||
|
||||
let recipients = rendered_msg.recipients.join("\x1e");
|
||||
let recipients = recipients.join("\x1e");
|
||||
param.set(Param::File, blob.as_name());
|
||||
param.set(Param::Recipients, &recipients);
|
||||
|
||||
@@ -1153,7 +1233,7 @@ pub fn interrupt_smtp_idle(context: &Context) {
|
||||
///
|
||||
/// Load jobs for this "[Thread]", i.e. either load SMTP jobs or load
|
||||
/// IMAP jobs. The `probe_network` parameter decides how to query
|
||||
/// jobs, this is tricky and probably wrong currently. Look at the
|
||||
/// jobs, this is tricky and probably wrong currently. Look at the
|
||||
/// SQL queries for details.
|
||||
fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Option<Job> {
|
||||
let query = if !probe_network {
|
||||
|
||||
@@ -143,7 +143,7 @@ impl JobThread {
|
||||
if state.jobs_needed {
|
||||
info!(
|
||||
context,
|
||||
"{}-IDLE will not be started as it was interrupted while not ideling.",
|
||||
"{}-IDLE will not be started as it was interrupted while not idling.",
|
||||
self.name,
|
||||
);
|
||||
state.jobs_needed = false;
|
||||
|
||||
261
src/key.rs
261
src/key.rs
@@ -4,14 +4,84 @@ use std::collections::BTreeMap;
|
||||
use std::io::Cursor;
|
||||
use std::path::Path;
|
||||
|
||||
use pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey};
|
||||
use pgp::composed::Deserializable;
|
||||
use pgp::ser::Serialize;
|
||||
use pgp::types::{KeyTrait, SecretKeyTrait};
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::sql::{self, Sql};
|
||||
use crate::sql::Sql;
|
||||
|
||||
// Re-export key types
|
||||
pub use crate::pgp::KeyPair;
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
|
||||
/// Error type for deltachat key handling.
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum Error {
|
||||
#[fail(display = "Could not decode base64")]
|
||||
Base64Decode(#[cause] base64::DecodeError, failure::Backtrace),
|
||||
#[fail(display = "rPGP error: {}", _0)]
|
||||
PgpError(#[cause] pgp::errors::Error, failure::Backtrace),
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for Error {
|
||||
fn from(err: base64::DecodeError) -> Error {
|
||||
Error::Base64Decode(err, failure::Backtrace::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pgp::errors::Error> for Error {
|
||||
fn from(err: pgp::errors::Error) -> Error {
|
||||
Error::PgpError(err, failure::Backtrace::new())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Convenience trait for working with keys.
|
||||
///
|
||||
/// This trait is implemented for rPGP's [SignedPublicKey] and
|
||||
/// [SignedSecretKey] types and makes working with them a little
|
||||
/// easier in the deltachat world.
|
||||
pub trait DcKey: Serialize + Deserializable {
|
||||
type KeyType: Serialize + Deserializable;
|
||||
|
||||
/// Create a key from some bytes.
|
||||
fn from_slice(bytes: &[u8]) -> Result<Self::KeyType> {
|
||||
Ok(<Self::KeyType as Deserializable>::from_bytes(Cursor::new(
|
||||
bytes,
|
||||
))?)
|
||||
}
|
||||
|
||||
/// Create a key from a base64 string.
|
||||
fn from_base64(data: &str) -> Result<Self::KeyType> {
|
||||
// strip newlines and other whitespace
|
||||
let cleaned: String = data.trim().split_whitespace().collect();
|
||||
let bytes = base64::decode(cleaned.as_bytes())?;
|
||||
Self::from_slice(&bytes)
|
||||
}
|
||||
|
||||
/// Serialise the key to a base64 string.
|
||||
fn to_base64(&self) -> String {
|
||||
// Not using Serialize::to_bytes() to make clear *why* it is
|
||||
// safe to ignore this error.
|
||||
// Because we write to a Vec<u8> the io::Write impls never
|
||||
// fail and we can hide this error.
|
||||
let mut buf = Vec::new();
|
||||
self.to_writer(&mut buf).unwrap();
|
||||
base64::encode(&buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl DcKey for SignedPublicKey {
|
||||
type KeyType = SignedPublicKey;
|
||||
}
|
||||
|
||||
impl DcKey for SignedSecretKey {
|
||||
type KeyType = SignedSecretKey;
|
||||
}
|
||||
|
||||
/// Cryptographic key
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
@@ -35,7 +105,7 @@ impl From<SignedSecretKey> for Key {
|
||||
impl std::convert::TryFrom<Key> for SignedSecretKey {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Key) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Key::Public(_) => Err(()),
|
||||
Key::Secret(key) => Ok(key),
|
||||
@@ -46,7 +116,7 @@ impl std::convert::TryFrom<Key> for SignedSecretKey {
|
||||
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &'a Key) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Key::Public(_) => Err(()),
|
||||
Key::Secret(key) => Ok(key),
|
||||
@@ -57,7 +127,7 @@ impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
|
||||
impl std::convert::TryFrom<Key> for SignedPublicKey {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Key) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Key::Public(key) => Ok(key),
|
||||
Key::Secret(_) => Err(()),
|
||||
@@ -68,7 +138,7 @@ impl std::convert::TryFrom<Key> for SignedPublicKey {
|
||||
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedPublicKey {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &'a Key) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Key::Public(key) => Ok(key),
|
||||
Key::Secret(_) => Err(()),
|
||||
@@ -92,7 +162,7 @@ impl Key {
|
||||
if bytes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let res: Result<Key, _> = match key_type {
|
||||
let res: std::result::Result<Key, _> = match key_type {
|
||||
KeyType::Public => SignedPublicKey::from_bytes(Cursor::new(bytes)).map(Into::into),
|
||||
KeyType::Private => SignedSecretKey::from_bytes(Cursor::new(bytes)).map(Into::into),
|
||||
};
|
||||
@@ -111,7 +181,7 @@ impl Key {
|
||||
key_type: KeyType,
|
||||
) -> Option<(Self, BTreeMap<String, String>)> {
|
||||
let bytes = data.as_bytes();
|
||||
let res: Result<(Key, _), _> = match key_type {
|
||||
let res: std::result::Result<(Key, _), _> = match key_type {
|
||||
KeyType::Public => SignedPublicKey::from_armor_single(Cursor::new(bytes))
|
||||
.map(|(k, h)| (Into::into(k), h)),
|
||||
KeyType::Private => SignedSecretKey::from_armor_single(Cursor::new(bytes))
|
||||
@@ -127,15 +197,6 @@ impl Key {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_base64(encoded_data: &str, key_type: KeyType) -> Option<Self> {
|
||||
// strip newlines and other whitespace
|
||||
let cleaned: String = encoded_data.trim().split_whitespace().collect();
|
||||
let bytes = cleaned.as_bytes();
|
||||
base64::decode(bytes)
|
||||
.ok()
|
||||
.and_then(|decoded| Self::from_slice(&decoded, key_type))
|
||||
}
|
||||
|
||||
pub fn from_self_public(
|
||||
context: &Context,
|
||||
self_addr: impl AsRef<str>,
|
||||
@@ -242,20 +303,97 @@ impl Key {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dc_key_save_self_keypair(
|
||||
/// Use of a [KeyPair] for encryption or decryption.
|
||||
///
|
||||
/// This is used by [store_self_keypair] to know what kind of key is
|
||||
/// being saved.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum KeyPairUse {
|
||||
/// The default key used to encrypt new messages.
|
||||
Default,
|
||||
/// Only used to decrypt existing message.
|
||||
ReadOnly,
|
||||
}
|
||||
|
||||
/// Error saving a keypair to the database.
|
||||
#[derive(Fail, Debug)]
|
||||
#[fail(display = "SaveKeyError: {}", message)]
|
||||
pub struct SaveKeyError {
|
||||
message: String,
|
||||
#[cause]
|
||||
cause: failure::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
}
|
||||
|
||||
impl SaveKeyError {
|
||||
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
cause: cause.into(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Store the keypair as an owned keypair for addr in the database.
|
||||
///
|
||||
/// This will save the keypair as keys for the given address. The
|
||||
/// "self" here refers to the fact that this DC instance owns the
|
||||
/// keypair. Usually `addr` will be [Config::ConfiguredAddr].
|
||||
///
|
||||
/// If either the public or private keys are already present in the
|
||||
/// database, this entry will be removed first regardless of the
|
||||
/// address associated with it. Practically this means saving the
|
||||
/// same key again overwrites it.
|
||||
///
|
||||
/// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr
|
||||
pub fn store_self_keypair(
|
||||
context: &Context,
|
||||
public_key: &Key,
|
||||
private_key: &Key,
|
||||
addr: impl AsRef<str>,
|
||||
is_default: bool,
|
||||
sql: &Sql,
|
||||
) -> bool {
|
||||
sql::execute(
|
||||
context,
|
||||
sql,
|
||||
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);",
|
||||
params![addr.as_ref(), is_default as i32, public_key.to_bytes(), private_key.to_bytes(), time()],
|
||||
).is_ok()
|
||||
keypair: &KeyPair,
|
||||
default: KeyPairUse,
|
||||
) -> std::result::Result<(), SaveKeyError> {
|
||||
// Everything should really be one transaction, more refactoring
|
||||
// is needed for that.
|
||||
let public_key = keypair
|
||||
.public
|
||||
.to_bytes()
|
||||
.map_err(|err| SaveKeyError::new("failed to serialise public key", err))?;
|
||||
let secret_key = keypair
|
||||
.secret
|
||||
.to_bytes()
|
||||
.map_err(|err| SaveKeyError::new("failed to serialise secret key", err))?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
|
||||
params![public_key, secret_key],
|
||||
)
|
||||
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
|
||||
if default == KeyPairUse::Default {
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE keypairs SET is_default=0;", params![])
|
||||
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
|
||||
}
|
||||
let is_default = match default {
|
||||
KeyPairUse::Default => true,
|
||||
KeyPairUse::ReadOnly => false,
|
||||
};
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
|
||||
VALUES (?,?,?,?,?);",
|
||||
params![
|
||||
keypair.addr.to_string(),
|
||||
is_default as i32,
|
||||
public_key,
|
||||
secret_key,
|
||||
time()
|
||||
],
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))
|
||||
}
|
||||
|
||||
/// Make a fingerprint human-readable, in hex format.
|
||||
@@ -287,6 +425,14 @@ pub fn dc_normalize_fingerprint(fp: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
static ref KEYPAIR: KeyPair = alice_keypair();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_fingerprint() {
|
||||
@@ -373,9 +519,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // is too expensive
|
||||
fn test_from_slice_roundtrip() {
|
||||
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap();
|
||||
let public_key = Key::from(KEYPAIR.public.clone());
|
||||
let private_key = Key::from(KEYPAIR.secret.clone());
|
||||
|
||||
let binary = public_key.to_bytes();
|
||||
let public_key2 = Key::from_slice(&binary, KeyType::Public).expect("invalid public key");
|
||||
@@ -408,9 +554,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // is too expensive
|
||||
fn test_ascii_roundtrip() {
|
||||
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap();
|
||||
let public_key = Key::from(KEYPAIR.public.clone());
|
||||
let private_key = Key::from(KEYPAIR.secret.clone());
|
||||
|
||||
let s = public_key.to_armored_string(None).unwrap();
|
||||
let (public_key2, _) =
|
||||
@@ -423,4 +569,51 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
Key::from_armored_string(&s, KeyType::Private).expect("invalid private key");
|
||||
assert_eq!(private_key, private_key2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_key() {
|
||||
let private_key = Key::from(KEYPAIR.secret.clone());
|
||||
let public_wrapped = private_key.split_key().unwrap();
|
||||
let public = SignedPublicKey::try_from(public_wrapped).unwrap();
|
||||
assert_eq!(public.primary_key, KEYPAIR.public.primary_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_self_key_twice() {
|
||||
// Saving the same key twice should result in only one row in
|
||||
// the keypairs table.
|
||||
let t = dummy_context();
|
||||
let nrows = || {
|
||||
t.ctx
|
||||
.sql
|
||||
.query_get_value::<_, u32>(&t.ctx, "SELECT COUNT(*) FROM keypairs;", params![])
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(nrows(), 0);
|
||||
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
|
||||
assert_eq!(nrows(), 1);
|
||||
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
|
||||
assert_eq!(nrows(), 1);
|
||||
}
|
||||
|
||||
// Convenient way to create a new key if you need one, run with
|
||||
// `cargo test key::tests::gen_key`.
|
||||
// #[test]
|
||||
// fn gen_key() {
|
||||
// let name = "fiona";
|
||||
// let keypair = crate::pgp::create_keypair(
|
||||
// EmailAddress::new(&format!("{}@example.net", name)).unwrap(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// std::fs::write(
|
||||
// format!("test-data/key/{}-public.asc", name),
|
||||
// keypair.public.to_base64(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// std::fs::write(
|
||||
// format!("test-data/key/{}-secret.asc", name),
|
||||
// keypair.secret.to_base64(),
|
||||
// )
|
||||
// .unwrap();
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::constants::KeyType;
|
||||
use crate::context::Context;
|
||||
use crate::key::*;
|
||||
use crate::key::Key;
|
||||
use crate::sql::Sql;
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
|
||||
@@ -27,23 +27,23 @@ pub(crate) mod events;
|
||||
pub use events::*;
|
||||
|
||||
mod aheader;
|
||||
pub mod blob;
|
||||
mod blob;
|
||||
pub mod chat;
|
||||
pub mod chatlist;
|
||||
pub mod config;
|
||||
pub mod configure;
|
||||
mod configure;
|
||||
pub mod constants;
|
||||
pub mod contact;
|
||||
pub mod context;
|
||||
mod e2ee;
|
||||
pub mod export_chat;
|
||||
mod imap;
|
||||
mod imap_client;
|
||||
pub mod imex;
|
||||
#[macro_use]
|
||||
pub mod job;
|
||||
mod job_thread;
|
||||
pub mod key;
|
||||
pub mod keyring;
|
||||
mod keyring;
|
||||
pub mod location;
|
||||
mod login_param;
|
||||
pub mod lot;
|
||||
@@ -54,6 +54,7 @@ pub mod oauth2;
|
||||
mod param;
|
||||
pub mod peerstate;
|
||||
pub mod pgp;
|
||||
pub mod provider;
|
||||
pub mod qr;
|
||||
pub mod securejoin;
|
||||
mod simplify;
|
||||
|
||||
@@ -64,11 +64,10 @@ impl Kml {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn parse(context: &Context, content: &[u8]) -> Result<Self, Error> {
|
||||
ensure!(content.len() <= 1024 * 1024, "kml-file is too large");
|
||||
pub fn parse(context: &Context, to_parse: &[u8]) -> Result<Self, Error> {
|
||||
ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large");
|
||||
|
||||
let to_parse = String::from_utf8_lossy(content);
|
||||
let mut reader = quick_xml::Reader::from_str(&to_parse);
|
||||
let mut reader = quick_xml::Reader::from_reader(to_parse);
|
||||
reader.trim_text(true);
|
||||
|
||||
let mut kml = Kml::new();
|
||||
@@ -365,6 +364,7 @@ fn is_marker(txt: &str) -> bool {
|
||||
txt.len() == 1 && !txt.starts_with(' ')
|
||||
}
|
||||
|
||||
/// Deletes all locations from the database.
|
||||
pub fn delete_all(context: &Context) -> Result<(), Error> {
|
||||
sql::execute(context, &context.sql, "DELETE FROM locations;", params![])?;
|
||||
context.call_cb(Event::LocationChanged(None));
|
||||
@@ -548,7 +548,7 @@ pub fn save(
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
|
||||
pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
|
||||
let now = time();
|
||||
let mut continue_streaming = false;
|
||||
info!(
|
||||
@@ -639,7 +639,7 @@ pub fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status {
|
||||
pub(crate) fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status {
|
||||
// this function is called when location-streaming _might_ have ended for a chat.
|
||||
// the function checks, if location-streaming is really ended;
|
||||
// if so, a device-message is added if not yet done.
|
||||
|
||||
@@ -258,10 +258,8 @@ fn get_readable_flags(flags: i32) -> String {
|
||||
res
|
||||
}
|
||||
|
||||
pub fn dc_build_tls(
|
||||
certificate_checks: CertificateChecks,
|
||||
) -> Result<native_tls::TlsConnector, native_tls::Error> {
|
||||
let mut tls_builder = native_tls::TlsConnector::builder();
|
||||
pub fn dc_build_tls(certificate_checks: CertificateChecks) -> async_native_tls::TlsConnector {
|
||||
let tls_builder = async_native_tls::TlsConnector::new();
|
||||
match certificate_checks {
|
||||
CertificateChecks::Automatic => {
|
||||
// Same as AcceptInvalidCertificates for now.
|
||||
@@ -270,13 +268,12 @@ pub fn dc_build_tls(
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.danger_accept_invalid_certs(true)
|
||||
}
|
||||
CertificateChecks::Strict => &mut tls_builder,
|
||||
CertificateChecks::Strict => tls_builder,
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => tls_builder
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.danger_accept_invalid_certs(true),
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -86,6 +86,9 @@ pub enum LotState {
|
||||
/// test1=formatted fingerprint
|
||||
QrFprWithoutAddr = 230,
|
||||
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
/// id=contact
|
||||
QrAddr = 320,
|
||||
|
||||
|
||||
194
src/message.rs
194
src/message.rs
@@ -4,6 +4,8 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use failure::Fail;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::constants::*;
|
||||
@@ -20,6 +22,10 @@ use crate::pgp::*;
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
lazy_static! {
|
||||
static ref UNWRAP_RE: regex::Regex = regex::Regex::new(r"\s+").unwrap();
|
||||
}
|
||||
|
||||
// In practice, the user additionally cuts the string themselves
|
||||
// pixel-accurate.
|
||||
const SUMMARY_CHARACTERS: usize = 160;
|
||||
@@ -29,7 +35,9 @@ const SUMMARY_CHARACTERS: usize = 160;
|
||||
/// Some message IDs are reserved to identify special message types.
|
||||
/// This type can represent both the special as well as normal
|
||||
/// messages.
|
||||
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
|
||||
#[derive(
|
||||
Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct MsgId(u32);
|
||||
|
||||
impl MsgId {
|
||||
@@ -47,10 +55,7 @@ impl MsgId {
|
||||
///
|
||||
/// This kind of message ID can not be used for real messages.
|
||||
pub fn is_special(self) -> bool {
|
||||
match self.0 {
|
||||
0..=DC_MSG_ID_LAST_SPECIAL => true,
|
||||
_ => false,
|
||||
}
|
||||
self.0 <= DC_MSG_ID_LAST_SPECIAL
|
||||
}
|
||||
|
||||
/// Whether the message ID is unset.
|
||||
@@ -80,6 +85,55 @@ impl MsgId {
|
||||
self.0 == DC_MSG_ID_DAYMARKER
|
||||
}
|
||||
|
||||
/// Put message into trash chat and delete message text.
|
||||
///
|
||||
/// It means the message is deleted locally, but not on the server
|
||||
/// yet.
|
||||
pub fn trash(self, context: &Context) -> crate::sql::Result<()> {
|
||||
let chat_id = ChatId::new(DC_CHAT_ID_TRASH);
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs SET chat_id=?, txt='', txt_raw='' WHERE id=?",
|
||||
params![chat_id, self],
|
||||
)
|
||||
}
|
||||
|
||||
/// Deletes a message and corresponding MDNs from the database.
|
||||
pub fn delete_from_db(self, context: &Context) -> crate::sql::Result<()> {
|
||||
// We don't use transactions yet, so remove MDNs first to make
|
||||
// sure they are not left while the message is deleted.
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM msgs_mdns WHERE msg_id=?;",
|
||||
params![self],
|
||||
)?;
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM msgs WHERE id=?;",
|
||||
params![self],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes IMAP server UID and folder from the database record.
|
||||
///
|
||||
/// It is used to avoid trying to remove the message from the
|
||||
/// server multiple times when there are multiple message records
|
||||
/// pointing to the same server UID.
|
||||
pub(crate) fn unlink(self, context: &Context) -> sql::Result<()> {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs \
|
||||
SET server_folder='', server_uid=0 \
|
||||
WHERE id=?",
|
||||
params![self],
|
||||
)
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
///
|
||||
/// Avoid using this, eventually types should be cleaned up enough
|
||||
@@ -148,9 +202,20 @@ impl rusqlite::types::FromSql for MsgId {
|
||||
#[fail(display = "Invalid Message ID.")]
|
||||
pub struct InvalidMsgId;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum MessengerMessage {
|
||||
pub(crate) enum MessengerMessage {
|
||||
No = 0,
|
||||
Yes = 1,
|
||||
|
||||
@@ -171,7 +236,7 @@ impl Default for MessengerMessage {
|
||||
/// to check if a mail was sent, use dc_msg_is_sent()
|
||||
/// approx. max. length returned by dc_msg_get_text()
|
||||
/// approx. max. length returned by dc_get_msg_info()
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
pub(crate) id: MsgId,
|
||||
pub(crate) from_id: u32,
|
||||
@@ -289,25 +354,6 @@ impl Message {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn delete_from_db(context: &Context, msg_id: MsgId) {
|
||||
if let Ok(msg) = Message::load_from_db(context, msg_id) {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM msgs WHERE id=?;",
|
||||
params![msg.id],
|
||||
)
|
||||
.ok();
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM msgs_mdns WHERE msg_id=?;",
|
||||
params![msg.id],
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_filemime(&self) -> Option<String> {
|
||||
if let Some(m) = self.param.get(Param::MimeType) {
|
||||
return Some(m.to_string());
|
||||
@@ -427,7 +473,7 @@ impl Message {
|
||||
pub fn get_text(&self) -> Option<String> {
|
||||
self.text
|
||||
.as_ref()
|
||||
.map(|text| dc_truncate(text, 30000, false).to_string())
|
||||
.map(|text| dc_truncate(text, 30000).to_string())
|
||||
}
|
||||
|
||||
pub fn get_filename(&self) -> Option<String> {
|
||||
@@ -610,7 +656,19 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
ToSql,
|
||||
FromSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
pub enum MessageState {
|
||||
Undefined = 0,
|
||||
@@ -786,7 +844,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
|
||||
return ret;
|
||||
}
|
||||
let rawtxt = rawtxt.unwrap_or_default();
|
||||
let rawtxt = dc_truncate(rawtxt.trim(), 100_000, false);
|
||||
let rawtxt = dc_truncate(rawtxt.trim(), 100_000);
|
||||
|
||||
let fts = dc_timestamp_to_str(msg.get_timestamp());
|
||||
ret += &format!("Sent: {}", fts);
|
||||
@@ -923,13 +981,15 @@ pub fn get_mime_headers(context: &Context, msg_id: MsgId) -> Option<String> {
|
||||
}
|
||||
|
||||
pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
|
||||
for msg_id in msg_ids.iter() {
|
||||
for msg_id in msg_ids {
|
||||
if let Ok(msg) = Message::load_from_db(context, *msg_id) {
|
||||
if msg.location_id > 0 {
|
||||
delete_poi_location(context, msg.location_id);
|
||||
}
|
||||
}
|
||||
update_msg_chat_id(context, *msg_id, ChatId::new(DC_CHAT_ID_TRASH));
|
||||
if let Err(err) = msg_id.trash(context) {
|
||||
error!(context, "Unable to trash message {}: {}", msg_id, err);
|
||||
}
|
||||
job_add(
|
||||
context,
|
||||
Action::DeleteMsgOnImap,
|
||||
@@ -949,16 +1009,6 @@ pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
|
||||
};
|
||||
}
|
||||
|
||||
fn update_msg_chat_id(context: &Context, msg_id: MsgId, chat_id: ChatId) -> bool {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs SET chat_id=? WHERE id=?;",
|
||||
params![chat_id, msg_id],
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn delete_poi_location(context: &Context, location_id: u32) -> bool {
|
||||
sql::execute(
|
||||
context,
|
||||
@@ -1118,18 +1168,20 @@ pub fn get_summarytext_by_raw(
|
||||
return prefix;
|
||||
}
|
||||
|
||||
if let Some(text) = text {
|
||||
let summary = if let Some(text) = text {
|
||||
if text.as_ref().is_empty() {
|
||||
prefix
|
||||
} else if prefix.is_empty() {
|
||||
dc_truncate(text.as_ref(), approx_characters, true).to_string()
|
||||
dc_truncate(text.as_ref(), approx_characters).to_string()
|
||||
} else {
|
||||
let tmp = format!("{} – {}", prefix, text.as_ref());
|
||||
dc_truncate(&tmp, approx_characters, true).to_string()
|
||||
dc_truncate(&tmp, approx_characters).to_string()
|
||||
}
|
||||
} else {
|
||||
prefix
|
||||
}
|
||||
};
|
||||
|
||||
UNWRAP_RE.replace_all(&summary, " ").to_string()
|
||||
}
|
||||
|
||||
// as we do not cut inside words, this results in about 32-42 characters.
|
||||
@@ -1317,10 +1369,55 @@ pub fn get_deaddrop_msg_cnt(context: &Context) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_deletion_cnt(
|
||||
context: &Context,
|
||||
from_server: bool,
|
||||
seconds: i64,
|
||||
) -> Result<usize, Error> {
|
||||
let self_chat_id = chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
let threshold_timestamp = time() - seconds;
|
||||
|
||||
let cnt: isize = if from_server {
|
||||
context.sql.query_row(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs m
|
||||
WHERE m.id > ?
|
||||
AND timestamp < ?
|
||||
AND chat_id != ?
|
||||
AND server_uid != 0;",
|
||||
params![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id],
|
||||
|row| row.get(0),
|
||||
)?
|
||||
} else {
|
||||
context.sql.query_row(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs m
|
||||
WHERE m.id > ?
|
||||
AND timestamp < ?
|
||||
AND chat_id != ?
|
||||
AND chat_id != ? AND hidden = 0;",
|
||||
params![
|
||||
DC_MSG_ID_LAST_SPECIAL,
|
||||
threshold_timestamp,
|
||||
self_chat_id,
|
||||
ChatId::new(DC_CHAT_ID_TRASH)
|
||||
],
|
||||
|row| row.get(0),
|
||||
)?
|
||||
};
|
||||
Ok(cnt as usize)
|
||||
}
|
||||
|
||||
/// Counts number of database records pointing to specified
|
||||
/// Message-ID.
|
||||
///
|
||||
/// Unlinked messages are excluded.
|
||||
pub fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 {
|
||||
// check the number of messages with the same rfc724_mid
|
||||
match context.sql.query_row(
|
||||
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=?;",
|
||||
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0",
|
||||
&[rfc724_mid],
|
||||
|row| row.get(0),
|
||||
) {
|
||||
@@ -1361,7 +1458,8 @@ pub fn update_server_uid(
|
||||
server_uid: u32,
|
||||
) {
|
||||
match context.sql.execute(
|
||||
"UPDATE msgs SET server_folder=?, server_uid=? WHERE rfc724_mid=?;",
|
||||
"UPDATE msgs SET server_folder=?, server_uid=? \
|
||||
WHERE rfc724_mid=?",
|
||||
params![server_folder.as_ref(), server_uid, rfc724_mid],
|
||||
) {
|
||||
Ok(_) => {}
|
||||
|
||||
@@ -17,6 +17,13 @@ use crate::param::*;
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
// attachments of 25 mb brutto should work on the majority of providers
|
||||
// (brutto examples: web.de=50, 1&1=40, t-online.de=32, gmail=25, posteo=50, yahoo=25, all-inkl=100).
|
||||
// as an upper limit, we double the size; the core won't send messages larger than this
|
||||
// to get the netto sizes, we subtract 1 mb header-overhead and the base64-overhead.
|
||||
pub const RECOMMENDED_FILE_SIZE: u64 = 24 * 1024 * 1024 / 4 * 3;
|
||||
const UPPER_LIMIT_FILE_SIZE: u64 = 49 * 1024 * 1024 / 4 * 3;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Loaded {
|
||||
Message { chat: Chat },
|
||||
@@ -53,9 +60,6 @@ pub struct RenderedEmail {
|
||||
pub is_gossiped: bool,
|
||||
pub last_added_location_id: u32,
|
||||
|
||||
pub from: String,
|
||||
pub recipients: Vec<String>,
|
||||
|
||||
/// Message ID (Message in the sense of Email)
|
||||
pub rfc724_mid: String,
|
||||
}
|
||||
@@ -64,7 +68,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
pub fn from_msg(
|
||||
context: &'a Context,
|
||||
msg: &'b Message,
|
||||
add_selfavatar: bool,
|
||||
attach_selfavatar: bool,
|
||||
) -> Result<MimeFactory<'a, 'b>, Error> {
|
||||
let chat = Chat::load_from_db(context, msg.chat_id)?;
|
||||
|
||||
@@ -152,7 +156,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
references,
|
||||
req_mdn,
|
||||
last_added_location_id: 0,
|
||||
attach_selfavatar: add_selfavatar,
|
||||
attach_selfavatar,
|
||||
context,
|
||||
};
|
||||
Ok(factory)
|
||||
@@ -342,6 +346,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recipients(&self) -> Vec<String> {
|
||||
self.recipients
|
||||
.iter()
|
||||
.map(|(_, addr)| addr.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn render(mut self) -> Result<RenderedEmail, Error> {
|
||||
// Headers that are encrypted
|
||||
// - Chat-*, except Chat-Version
|
||||
@@ -357,7 +368,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
self.from_addr.clone(),
|
||||
);
|
||||
|
||||
let mut to = Vec::with_capacity(self.recipients.len());
|
||||
let mut to = Vec::new();
|
||||
for (name, addr) in self.recipients.iter() {
|
||||
if name.is_empty() {
|
||||
to.push(Address::new_mailbox(addr.clone()));
|
||||
@@ -369,6 +380,10 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
if to.is_empty() {
|
||||
to.push(from.clone());
|
||||
}
|
||||
|
||||
if !self.references.is_empty() {
|
||||
unprotected_headers.push(Header::new("References".into(), self.references.clone()));
|
||||
}
|
||||
@@ -558,8 +573,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
};
|
||||
|
||||
let MimeFactory {
|
||||
recipients,
|
||||
from_addr,
|
||||
last_added_location_id,
|
||||
..
|
||||
} = self;
|
||||
@@ -570,8 +583,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
is_encrypted,
|
||||
is_gossiped,
|
||||
last_added_location_id,
|
||||
recipients: recipients.into_iter().map(|(_, addr)| addr).collect(),
|
||||
from: from_addr,
|
||||
rfc724_mid,
|
||||
})
|
||||
}
|
||||
@@ -595,6 +606,27 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
Some(part)
|
||||
}
|
||||
|
||||
fn get_location_kml_part(&mut self) -> Result<PartBuilder, Error> {
|
||||
let (kml_content, last_added_location_id) =
|
||||
location::get_kml(self.context, self.msg.chat_id)?;
|
||||
let part = PartBuilder::new()
|
||||
.content_type(
|
||||
&"application/vnd.google-earth.kml+xml"
|
||||
.parse::<mime::Mime>()
|
||||
.unwrap(),
|
||||
)
|
||||
.header((
|
||||
"Content-Disposition",
|
||||
"attachment; filename=\"location.kml\"",
|
||||
))
|
||||
.body(kml_content);
|
||||
if !self.msg.param.exists(Param::SetLatitude) {
|
||||
// otherwise, the independent location is already filed
|
||||
self.last_added_location_id = last_added_location_id;
|
||||
}
|
||||
Ok(part)
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn render_message(
|
||||
&mut self,
|
||||
@@ -610,7 +642,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let command = self.msg.param.get_cmd();
|
||||
let mut placeholdertext = None;
|
||||
let mut meta_part = None;
|
||||
let mut add_compatibility_header = false;
|
||||
|
||||
if chat.typ == Chattype::VerifiedGroup {
|
||||
protected_headers.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
|
||||
@@ -653,7 +684,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
"vg-member-added".to_string(),
|
||||
));
|
||||
}
|
||||
add_compatibility_header = true;
|
||||
}
|
||||
SystemMessage::GroupNameChanged => {
|
||||
let value_to_add = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||
@@ -674,7 +704,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
"0".to_string(),
|
||||
));
|
||||
}
|
||||
add_compatibility_header = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -742,18 +771,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
|
||||
let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image")?;
|
||||
meta_part = Some(mail);
|
||||
protected_headers.push(Header::new(
|
||||
"Chat-Group-Avatar".into(),
|
||||
filename_as_sent.clone(),
|
||||
));
|
||||
|
||||
// add the old group-image headers for versions <=0.973 resp. <=beta.15 (december 2019)
|
||||
// image deletion is not supported in the compatibility layer.
|
||||
// this can be removed some time after releasing 1.0,
|
||||
// grep for #DeprecatedAvatar to get the place where compatibility parsing takes place.
|
||||
if add_compatibility_header {
|
||||
protected_headers.push(Header::new("Chat-Group-Image".into(), filename_as_sent));
|
||||
}
|
||||
protected_headers.push(Header::new("Chat-Group-Avatar".into(), filename_as_sent));
|
||||
}
|
||||
|
||||
if self.msg.viewtype == Viewtype::Sticker {
|
||||
@@ -825,7 +843,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
if !is_file_size_okay(context, &self.msg) {
|
||||
bail!(
|
||||
"Message exceeds the recommended {} MB.",
|
||||
24 * 1024 * 1024 / 4 * 3 / 1000 / 1000,
|
||||
RECOMMENDED_FILE_SIZE / 1_000_000,
|
||||
);
|
||||
} else {
|
||||
let (file_part, _) = build_body_file(context, &self.msg, "")?;
|
||||
@@ -842,35 +860,17 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
|
||||
if location::is_sending_locations_to_chat(context, self.msg.chat_id) {
|
||||
match location::get_kml(context, self.msg.chat_id) {
|
||||
Ok((kml_content, last_added_location_id)) => {
|
||||
parts.push(
|
||||
PartBuilder::new()
|
||||
.content_type(
|
||||
&"application/vnd.google-earth.kml+xml"
|
||||
.parse::<mime::Mime>()
|
||||
.unwrap(),
|
||||
)
|
||||
.header((
|
||||
"Content-Disposition",
|
||||
"attachment; filename=\"location.kml\"",
|
||||
))
|
||||
.body(kml_content),
|
||||
);
|
||||
if !self.msg.param.exists(Param::SetLatitude) {
|
||||
// otherwise, the independent location is already filed
|
||||
self.last_added_location_id = last_added_location_id;
|
||||
}
|
||||
}
|
||||
match self.get_location_kml_part() {
|
||||
Ok(part) => parts.push(part),
|
||||
Err(err) => {
|
||||
warn!(context, "mimefactory: could not get location: {}", err);
|
||||
warn!(context, "mimefactory: could not send location: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.attach_selfavatar {
|
||||
match context.get_config(Config::Selfavatar) {
|
||||
Some(path) => match build_selfavatar_file(context, path) {
|
||||
Some(path) => match build_selfavatar_file(context, &path) {
|
||||
Ok((part, filename)) => {
|
||||
parts.push(part);
|
||||
protected_headers.push(Header::new("Chat-User-Avatar".into(), filename))
|
||||
@@ -1065,7 +1065,7 @@ fn build_body_file(
|
||||
Ok((mail, filename_to_send))
|
||||
}
|
||||
|
||||
fn build_selfavatar_file(context: &Context, path: String) -> Result<(PartBuilder, String), Error> {
|
||||
fn build_selfavatar_file(context: &Context, path: &str) -> Result<(PartBuilder, String), Error> {
|
||||
let blob = BlobObject::from_path(context, path)?;
|
||||
let filename_to_send = match blob.suffix() {
|
||||
Some(suffix) => format!("avatar.{}", suffix),
|
||||
@@ -1101,7 +1101,7 @@ fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
|
||||
match msg.param.get_path(Param::File, context).unwrap_or(None) {
|
||||
Some(path) => {
|
||||
let bytes = dc_get_filebytes(context, &path);
|
||||
bytes <= (49 * 1024 * 1024 / 4 * 3)
|
||||
bytes <= UPPER_LIMIT_FILE_SIZE
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -311,9 +311,15 @@ impl Oauth2 {
|
||||
return None;
|
||||
}
|
||||
if let Ok(response) = parsed {
|
||||
// serde_json::Value.as_str() removes the quotes of json-strings
|
||||
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
|
||||
// but serde_json::Value.to_string() does not!
|
||||
if let Some(addr) = response.get("email") {
|
||||
Some(addr.to_string())
|
||||
if let Some(s) = addr.as_str() {
|
||||
Some(s.to_string())
|
||||
} else {
|
||||
warn!(context, "E-mail in userinfo is not a string: {}", addr);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
warn!(context, "E-mail missing in userinfo.");
|
||||
None
|
||||
|
||||
13
src/param.rs
13
src/param.rs
@@ -4,6 +4,7 @@ use std::path::PathBuf;
|
||||
use std::str;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::blob::{BlobError, BlobObject};
|
||||
use crate::context::Context;
|
||||
@@ -12,7 +13,9 @@ use crate::message::MsgId;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
|
||||
/// Available param keys.
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash, PartialOrd, Ord, FromPrimitive)]
|
||||
#[derive(
|
||||
PartialEq, Eq, Debug, Clone, Copy, Hash, PartialOrd, Ord, FromPrimitive, Serialize, Deserialize,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum Param {
|
||||
/// For messages and jobs
|
||||
@@ -85,12 +88,6 @@ pub enum Param {
|
||||
/// For Jobs
|
||||
SetLongitude = b'n',
|
||||
|
||||
/// For Jobs
|
||||
ServerFolder = b'Z',
|
||||
|
||||
/// For Jobs
|
||||
ServerUid = b'z',
|
||||
|
||||
/// For Jobs
|
||||
AlsoMove = b'M',
|
||||
|
||||
@@ -135,7 +132,7 @@ pub enum ForcePlaintext {
|
||||
/// The structure is serialized by calling `to_string()` on it.
|
||||
///
|
||||
/// Only for library-internal use.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct Params {
|
||||
inner: BTreeMap<Param, String>,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
@@ -7,7 +8,7 @@ use num_traits::FromPrimitive;
|
||||
use crate::aheader::*;
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::key::*;
|
||||
use crate::key::{Key, SignedPublicKey};
|
||||
use crate::sql::{self, Sql};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -126,7 +127,7 @@ impl<'a> Peerstate<'a> {
|
||||
res.last_seen_autocrypt = message_time;
|
||||
res.to_save = Some(ToSave::All);
|
||||
res.prefer_encrypt = header.prefer_encrypt;
|
||||
res.public_key = Some(header.public_key.clone());
|
||||
res.public_key = Some(Key::from(header.public_key.clone()));
|
||||
res.recalc_fingerprint();
|
||||
|
||||
res
|
||||
@@ -137,7 +138,7 @@ impl<'a> Peerstate<'a> {
|
||||
|
||||
res.gossip_timestamp = message_time;
|
||||
res.to_save = Some(ToSave::All);
|
||||
res.gossip_key = Some(gossip_header.public_key.clone());
|
||||
res.gossip_key = Some(Key::from(gossip_header.public_key.clone()));
|
||||
res.recalc_fingerprint();
|
||||
|
||||
res
|
||||
@@ -293,8 +294,8 @@ impl<'a> Peerstate<'a> {
|
||||
self.to_save = Some(ToSave::All)
|
||||
}
|
||||
|
||||
if self.public_key.as_ref() != Some(&header.public_key) {
|
||||
self.public_key = Some(header.public_key.clone());
|
||||
if self.public_key.as_ref() != Some(&Key::from(header.public_key.clone())) {
|
||||
self.public_key = Some(Key::from(header.public_key.clone()));
|
||||
self.recalc_fingerprint();
|
||||
self.to_save = Some(ToSave::All);
|
||||
}
|
||||
@@ -309,21 +310,50 @@ impl<'a> Peerstate<'a> {
|
||||
if message_time > self.gossip_timestamp {
|
||||
self.gossip_timestamp = message_time;
|
||||
self.to_save = Some(ToSave::Timestamps);
|
||||
if self.gossip_key.as_ref() != Some(&gossip_header.public_key) {
|
||||
self.gossip_key = Some(gossip_header.public_key.clone());
|
||||
let hdr_key = Key::from(gossip_header.public_key.clone());
|
||||
if self.gossip_key.as_ref() != Some(&hdr_key) {
|
||||
self.gossip_key = Some(hdr_key);
|
||||
self.recalc_fingerprint();
|
||||
self.to_save = Some(ToSave::All)
|
||||
}
|
||||
|
||||
// This is non-standard.
|
||||
//
|
||||
// According to Autocrypt 1.1.0 gossip headers SHOULD NOT
|
||||
// contain encryption preference, but we include it into
|
||||
// Autocrypt-Gossip and apply it one way (from
|
||||
// "nopreference" to "mutual").
|
||||
//
|
||||
// This is compatible to standard clients, because they
|
||||
// can't distinguish it from the case where we have
|
||||
// contacted the client in the past and received this
|
||||
// preference via Autocrypt header.
|
||||
if self.last_seen_autocrypt == 0
|
||||
&& self.prefer_encrypt == EncryptPreference::NoPreference
|
||||
&& gossip_header.prefer_encrypt == EncryptPreference::Mutual
|
||||
{
|
||||
self.prefer_encrypt = EncryptPreference::Mutual;
|
||||
self.to_save = Some(ToSave::All);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn render_gossip_header(&self, min_verified: PeerstateVerifiedStatus) -> Option<String> {
|
||||
if let Some(key) = self.peek_key(min_verified) {
|
||||
// TODO: avoid cloning
|
||||
let public_key = SignedPublicKey::try_from(key.clone()).ok()?;
|
||||
let header = Aheader::new(
|
||||
self.addr.clone(),
|
||||
key.clone(),
|
||||
EncryptPreference::NoPreference,
|
||||
public_key,
|
||||
// Autocrypt 1.1.0 specification says that
|
||||
// `prefer-encrypt` attribute SHOULD NOT be included,
|
||||
// but we include it anyway to propagate encryption
|
||||
// preference to new members in group chats.
|
||||
if self.last_seen_autocrypt > 0 {
|
||||
self.prefer_encrypt
|
||||
} else {
|
||||
EncryptPreference::NoPreference
|
||||
},
|
||||
);
|
||||
Some(header.to_string())
|
||||
} else {
|
||||
@@ -332,18 +362,13 @@ impl<'a> Peerstate<'a> {
|
||||
}
|
||||
|
||||
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Key> {
|
||||
if self.public_key.is_none() && self.gossip_key.is_none() && self.verified_key.is_none() {
|
||||
return None;
|
||||
match min_verified {
|
||||
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(),
|
||||
PeerstateVerifiedStatus::Unverified => self
|
||||
.public_key
|
||||
.as_ref()
|
||||
.or_else(|| self.gossip_key.as_ref()),
|
||||
}
|
||||
|
||||
if min_verified != PeerstateVerifiedStatus::Unverified {
|
||||
return self.verified_key.as_ref();
|
||||
}
|
||||
if self.public_key.is_some() {
|
||||
return self.public_key.as_ref();
|
||||
}
|
||||
|
||||
self.gossip_key.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_verified(
|
||||
@@ -450,8 +475,8 @@ impl<'a> Peerstate<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
@@ -459,11 +484,7 @@ mod tests {
|
||||
let ctx = crate::test_utils::dummy_context();
|
||||
let addr = "hello@mail.com";
|
||||
|
||||
let pub_key = crate::key::Key::from_base64(
|
||||
include_str!("../test-data/key/public.asc"),
|
||||
KeyType::Public,
|
||||
)
|
||||
.unwrap();
|
||||
let pub_key = crate::key::Key::from(alice_keypair().public);
|
||||
|
||||
let mut peerstate = Peerstate {
|
||||
context: &ctx.ctx,
|
||||
@@ -503,12 +524,7 @@ mod tests {
|
||||
fn test_peerstate_double_create() {
|
||||
let ctx = crate::test_utils::dummy_context();
|
||||
let addr = "hello@mail.com";
|
||||
|
||||
let pub_key = crate::key::Key::from_base64(
|
||||
include_str!("../test-data/key/public.asc"),
|
||||
KeyType::Public,
|
||||
)
|
||||
.unwrap();
|
||||
let pub_key = crate::key::Key::from(alice_keypair().public);
|
||||
|
||||
let peerstate = Peerstate {
|
||||
context: &ctx.ctx,
|
||||
@@ -542,11 +558,7 @@ mod tests {
|
||||
let ctx = crate::test_utils::dummy_context();
|
||||
let addr = "hello@mail.com";
|
||||
|
||||
let pub_key = crate::key::Key::from_base64(
|
||||
include_str!("../test-data/key/public.asc"),
|
||||
KeyType::Public,
|
||||
)
|
||||
.unwrap();
|
||||
let pub_key = crate::key::Key::from(alice_keypair().public);
|
||||
|
||||
let mut peerstate = Peerstate {
|
||||
context: &ctx.ctx,
|
||||
|
||||
258
src/pgp.rs
258
src/pgp.rs
@@ -16,6 +16,8 @@ use pgp::types::{
|
||||
};
|
||||
use rand::{thread_rng, CryptoRng, Rng};
|
||||
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::error::Result;
|
||||
use crate::key::*;
|
||||
use crate::keyring::*;
|
||||
@@ -111,12 +113,53 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
|
||||
Ok((typ, headers, bytes))
|
||||
}
|
||||
|
||||
/// Create a new key pair.
|
||||
pub fn create_keypair(addr: impl AsRef<str>) -> Option<(Key, Key)> {
|
||||
let user_id = format!("<{}>", addr.as_ref());
|
||||
/// Error with generating a PGP keypair.
|
||||
///
|
||||
/// Most of these are likely coding errors rather than user errors
|
||||
/// since all variability is hardcoded.
|
||||
#[derive(Fail, Debug)]
|
||||
#[fail(display = "PgpKeygenError: {}", message)]
|
||||
pub(crate) struct PgpKeygenError {
|
||||
message: String,
|
||||
#[cause]
|
||||
cause: failure::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
}
|
||||
|
||||
impl PgpKeygenError {
|
||||
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
cause: cause.into(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A PGP keypair.
|
||||
///
|
||||
/// This has it's own struct to be able to keep the public and secret
|
||||
/// keys together as they are one unit.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct KeyPair {
|
||||
pub addr: EmailAddress,
|
||||
pub public: SignedPublicKey,
|
||||
pub secret: SignedSecretKey,
|
||||
}
|
||||
|
||||
/// Create a new key pair.
|
||||
pub(crate) fn create_keypair(
|
||||
addr: EmailAddress,
|
||||
keygen_type: KeyGenType,
|
||||
) -> std::result::Result<KeyPair, PgpKeygenError> {
|
||||
let (secret_key_type, public_key_type) = match keygen_type {
|
||||
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
|
||||
};
|
||||
|
||||
let user_id = format!("<{}>", addr);
|
||||
let key_params = SecretKeyParamsBuilder::default()
|
||||
.key_type(PgpKeyType::Rsa(2048))
|
||||
.key_type(secret_key_type)
|
||||
.can_create_certificates(true)
|
||||
.can_sign(true)
|
||||
.primary_user_id(user_id)
|
||||
@@ -139,27 +182,36 @@ pub fn create_keypair(addr: impl AsRef<str>) -> Option<(Key, Key)> {
|
||||
])
|
||||
.subkey(
|
||||
SubkeyParamsBuilder::default()
|
||||
.key_type(PgpKeyType::Rsa(2048))
|
||||
.key_type(public_key_type)
|
||||
.can_encrypt(true)
|
||||
.passphrase(None)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.build()
|
||||
.expect("invalid key params");
|
||||
|
||||
let key = key_params.generate().expect("invalid params");
|
||||
.map_err(|err| PgpKeygenError::new("invalid key params", failure::err_msg(err)))?;
|
||||
let key = key_params
|
||||
.generate()
|
||||
.map_err(|err| PgpKeygenError::new("invalid params", err))?;
|
||||
let private_key = key.sign(|| "".into()).expect("failed to sign secret key");
|
||||
|
||||
let public_key = private_key.public_key();
|
||||
let public_key = public_key
|
||||
.sign(&private_key, || "".into())
|
||||
.expect("failed to sign public key");
|
||||
.map_err(|err| PgpKeygenError::new("failed to sign public key", err))?;
|
||||
|
||||
private_key.verify().expect("invalid private key generated");
|
||||
public_key.verify().expect("invalid public key generated");
|
||||
private_key
|
||||
.verify()
|
||||
.map_err(|err| PgpKeygenError::new("invalid private key generated", err))?;
|
||||
public_key
|
||||
.verify()
|
||||
.map_err(|err| PgpKeygenError::new("invalid public key generated", err))?;
|
||||
|
||||
Some((Key::Public(public_key), Key::Secret(private_key)))
|
||||
Ok(KeyPair {
|
||||
addr,
|
||||
public: public_key,
|
||||
secret: private_key,
|
||||
})
|
||||
}
|
||||
|
||||
/// Select public key or subkey to use for encryption.
|
||||
@@ -311,6 +363,8 @@ pub fn symm_decrypt<T: std::io::Read + std::io::Seek>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
#[test]
|
||||
fn test_split_armored_data_1() {
|
||||
@@ -338,4 +392,184 @@ mod tests {
|
||||
assert!(!base64.is_empty());
|
||||
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_keypair() {
|
||||
let keypair0 = create_keypair(
|
||||
EmailAddress::new("foo@bar.de").unwrap(),
|
||||
KeyGenType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
let keypair1 = create_keypair(
|
||||
EmailAddress::new("two@zwo.de").unwrap(),
|
||||
KeyGenType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(keypair0.public, keypair1.public);
|
||||
}
|
||||
|
||||
/// [Key] objects to use in tests.
|
||||
struct TestKeys {
|
||||
alice_secret: Key,
|
||||
alice_public: Key,
|
||||
bob_secret: Key,
|
||||
bob_public: Key,
|
||||
}
|
||||
|
||||
impl TestKeys {
|
||||
fn new() -> TestKeys {
|
||||
let alice = alice_keypair();
|
||||
let bob = bob_keypair();
|
||||
TestKeys {
|
||||
alice_secret: Key::from(alice.secret.clone()),
|
||||
alice_public: Key::from(alice.public.clone()),
|
||||
bob_secret: Key::from(bob.secret.clone()),
|
||||
bob_public: Key::from(bob.public.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The original text of [CTEXT_SIGNED]
|
||||
static CLEARTEXT: &[u8] = b"This is a test";
|
||||
|
||||
lazy_static! {
|
||||
/// Initialised [TestKeys] for tests.
|
||||
static ref KEYS: TestKeys = TestKeys::new();
|
||||
|
||||
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
|
||||
static ref CTEXT_SIGNED: String = {
|
||||
let mut keyring = Keyring::default();
|
||||
keyring.add_owned(KEYS.alice_public.clone());
|
||||
keyring.add_ref(&KEYS.bob_public);
|
||||
pk_encrypt(CLEARTEXT, &keyring, Some(&KEYS.alice_secret)).unwrap()
|
||||
};
|
||||
|
||||
/// A cyphertext encrypted to Alice & Bob, not signed.
|
||||
static ref CTEXT_UNSIGNED: String = {
|
||||
let mut keyring = Keyring::default();
|
||||
keyring.add_owned(KEYS.alice_public.clone());
|
||||
keyring.add_ref(&KEYS.bob_public);
|
||||
pk_encrypt(CLEARTEXT, &keyring, None).unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_signed() {
|
||||
assert!(!CTEXT_SIGNED.is_empty());
|
||||
assert!(CTEXT_SIGNED.starts_with("-----BEGIN PGP MESSAGE-----"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_unsigned() {
|
||||
assert!(!CTEXT_UNSIGNED.is_empty());
|
||||
assert!(CTEXT_UNSIGNED.starts_with("-----BEGIN PGP MESSAGE-----"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_singed() {
|
||||
// Check decrypting as Alice
|
||||
let mut decrypt_keyring = Keyring::default();
|
||||
decrypt_keyring.add_ref(&KEYS.alice_secret);
|
||||
let mut sig_check_keyring = Keyring::default();
|
||||
sig_check_keyring.add_ref(&KEYS.alice_public);
|
||||
let mut valid_signatures: HashSet<String> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes(),
|
||||
&decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
)
|
||||
.map_err(|err| println!("{:?}", err))
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 1);
|
||||
|
||||
// Check decrypting as Bob
|
||||
let mut decrypt_keyring = Keyring::default();
|
||||
decrypt_keyring.add_ref(&KEYS.bob_secret);
|
||||
let mut sig_check_keyring = Keyring::default();
|
||||
sig_check_keyring.add_ref(&KEYS.alice_public);
|
||||
let mut valid_signatures: HashSet<String> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes(),
|
||||
&decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
)
|
||||
.map_err(|err| println!("{:?}", err))
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_no_sig_check() {
|
||||
let mut keyring = Keyring::default();
|
||||
keyring.add_ref(&KEYS.alice_secret);
|
||||
let empty_keyring = Keyring::default();
|
||||
let mut valid_signatures: HashSet<String> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes(),
|
||||
&keyring,
|
||||
&empty_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_signed_no_key() {
|
||||
// The validation does not have the public key of the signer.
|
||||
let mut decrypt_keyring = Keyring::default();
|
||||
decrypt_keyring.add_ref(&KEYS.bob_secret);
|
||||
let mut sig_check_keyring = Keyring::default();
|
||||
sig_check_keyring.add_ref(&KEYS.bob_public);
|
||||
let mut valid_signatures: HashSet<String> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes(),
|
||||
&decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_unsigned() {
|
||||
let mut decrypt_keyring = Keyring::default();
|
||||
decrypt_keyring.add_ref(&KEYS.bob_secret);
|
||||
let sig_check_keyring = Keyring::default();
|
||||
decrypt_keyring.add_ref(&KEYS.alice_public);
|
||||
let mut valid_signatures: HashSet<String> = Default::default();
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_UNSIGNED.as_bytes(),
|
||||
&decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
Some(&mut valid_signatures),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_signed_no_sigret() {
|
||||
// Check decrypting signed cyphertext without providing the HashSet for signatures.
|
||||
let mut decrypt_keyring = Keyring::default();
|
||||
decrypt_keyring.add_ref(&KEYS.bob_secret);
|
||||
let mut sig_check_keyring = Keyring::default();
|
||||
sig_check_keyring.add_ref(&KEYS.alice_public);
|
||||
let plain = pk_decrypt(
|
||||
CTEXT_SIGNED.as_bytes(),
|
||||
&decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
}
|
||||
}
|
||||
|
||||
365
src/provider/data.rs
Normal file
365
src/provider/data.rs
Normal file
@@ -0,0 +1,365 @@
|
||||
// file generated by src/provider/update.py
|
||||
|
||||
use crate::provider::Protocol::*;
|
||||
use crate::provider::Socket::*;
|
||||
use crate::provider::UsernamePattern::*;
|
||||
use crate::provider::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
|
||||
// aktivix.org.md: aktivix.org
|
||||
static ref P_AKTIVIX_ORG: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/aktivix-org",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 25, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// aol.md: aol.com
|
||||
static ref P_AOL: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
before_login_hint: "To log in to AOL with Delta Chat, you need to set up an app password in the AOL web interface.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/aol",
|
||||
server: vec![
|
||||
],
|
||||
};
|
||||
|
||||
// autistici.org.md: autistici.org
|
||||
static ref P_AUTISTICI_ORG: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/autistici-org",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "mail.autistici.org", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "smtp.autistici.org", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// bluewin.ch.md: bluewin.ch
|
||||
static ref P_BLUEWIN_CH: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/bluewin-ch",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imaps.bluewin.ch", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "smtpauths.bluewin.ch", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// comcast.md: xfinity.com, comcast.net
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// dismail.de.md: dismail.de
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// disroot.md: disroot.org
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// example.com.md: example.com, example.org
|
||||
static ref P_EXAMPLE_COM: Provider = Provider {
|
||||
status: Status::BROKEN,
|
||||
before_login_hint: "Hush this provider doesn't exist!",
|
||||
after_login_hint: "This provider doesn't really exist, so you can't use it :/ If you need an email provider for Delta Chat, take a look at providers.delta.chat!",
|
||||
overview_page: "https://providers.delta.chat/example-com",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.example.com", port: 1337, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.example.com", port: 1337, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// fastmail.md: fastmail.com
|
||||
static ref P_FASTMAIL: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
before_login_hint: "You must create an app-specific password for Delta Chat before you can log in.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/fastmail",
|
||||
server: vec![
|
||||
],
|
||||
};
|
||||
|
||||
// freenet.de.md: freenet.de
|
||||
static ref P_FREENET_DE: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/freenet-de",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "mx.freenet.de", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "mx.freenet.de", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// gmail.md: gmail.com, googlemail.com
|
||||
static ref P_GMAIL: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
before_login_hint: "For Gmail accounts, you need to create an app-password if you have \"2-Step Verification\" enabled. If this setting is not available, you need to enable \"less secure apps\".",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/gmail",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.gmail.com", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "smtp.gmail.com", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// gmx.net.md: gmx.net, gmx.de, gmx.at, gmx.ch, gmx.org, gmx.eu, gmx.info, gmx.biz, gmx.com
|
||||
static ref P_GMX_NET: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
before_login_hint: "You must allow IMAP access to your account before you can login.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/gmx-net",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.gmx.net", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "mail.gmx.net", port: 465, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "mail.gmx.net", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// i.ua.md: i.ua
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// icloud.md: icloud.com, me.com, mac.com
|
||||
static ref P_ICLOUD: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
before_login_hint: "You must create an app-specific password for Delta Chat before you can login.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/icloud",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.mail.me.com", port: 993, username_pattern: EMAILLOCALPART },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.mail.me.com", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// kolst.com.md: kolst.com
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// kontent.com.md: kontent.com
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// mail.ru.md: mail.ru, inbox.ru, bk.ru, list.ru
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// mailbox.org.md: mailbox.org, secure.mailbox.org
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// nauta.cu.md: nauta.cu
|
||||
static ref P_NAUTA_CU: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "Atención - con nauta.cu, puede enviar mensajes sólo a un máximo de 20 personas a la vez. En grupos más grandes, no puede enviar mensajes o abandonar el grupo.",
|
||||
overview_page: "https://providers.delta.chat/nauta-cu",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "imap.nauta.cu", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.nauta.cu", port: 25, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
|
||||
static ref P_OUTLOOK_COM: Provider = Provider {
|
||||
status: Status::BROKEN,
|
||||
before_login_hint: "Outlook.com email addresses will not work as expected as these servers remove some important transport information. Hopefully sooner or later there will be a fix, for now we suggest to use another email address.",
|
||||
after_login_hint: "Outlook.com email addresses will not work as expected as these servers remove some important transport information. Unencrypted 1-on-1 chats kind of work, but groups and encryption don't. Hopefully sooner or later there will be a fix, for now we suggest to use another email address.",
|
||||
overview_page: "https://providers.delta.chat/outlook-com",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap-mail.outlook.com", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp-mail.outlook.com", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// posteo.md: posteo.de
|
||||
static ref P_POSTEO: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/posteo",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "posteo.de", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "posteo.de", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// protonmail.md: protonmail.com, protonmail.ch
|
||||
static ref P_PROTONMAIL: Provider = Provider {
|
||||
status: Status::BROKEN,
|
||||
before_login_hint: "Protonmail does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Protonmail.",
|
||||
after_login_hint: "To use Delta Chat with Protonmail, the IMAP bridge must be running in the background. If you have connectivity issues, double check whether it works as expected.",
|
||||
overview_page: "https://providers.delta.chat/protonmail",
|
||||
server: vec![
|
||||
],
|
||||
};
|
||||
|
||||
// riseup.net.md: riseup.net
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// rogers.com.md: rogers.com
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// t-online.md: t-online.de, magenta.de
|
||||
static ref P_T_ONLINE: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
before_login_hint: "To use Delta Chat with a T-Online email address, you need to create an app password in the web interface.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/t-online",
|
||||
server: vec![
|
||||
],
|
||||
};
|
||||
|
||||
// testrun.md: testrun.org
|
||||
static ref P_TESTRUN: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/testrun",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "testrun.org", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "testrun.org", port: 143, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "testrun.org", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// tiscali.it.md: tiscali.it
|
||||
static ref P_TISCALI_IT: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/tiscali-it",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.tiscali.it", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "smtp.tiscali.it", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// ukr.net.md: ukr.net
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// vfemail.md: vfemail.net
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// web.de.md: web.de, email.de, flirt.ms, hallo.ms, kuss.ms, love.ms, magic.ms, singles.ms, cool.ms, kanzler.ms, okay.ms, party.ms, pop.ms, stars.ms, techno.ms, clever.ms, deutschland.ms, genial.ms, ich.ms, online.ms, smart.ms, wichtig.ms, action.ms, fussball.ms, joker.ms, planet.ms, power.ms
|
||||
static ref P_WEB_DE: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
before_login_hint: "You must allow IMAP access to your account before you can login.",
|
||||
after_login_hint: "Note: if you have your web.de spam settings too strict, you won't receive contact requests from new people. If you want to receive contact requests, you should disable the \"3-Wege-Spamschutz\" in the web.de settings. Read how: https://hilfe.web.de/email/spam-und-viren/spamschutz-einstellungen.html",
|
||||
overview_page: "https://providers.delta.chat/web-de",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.web.de", port: 993, username_pattern: EMAILLOCALPART },
|
||||
Server { protocol: IMAP, socket: STARTTLS, hostname: "imap.web.de", port: 143, username_pattern: EMAILLOCALPART },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.web.de", port: 587, username_pattern: EMAILLOCALPART },
|
||||
],
|
||||
};
|
||||
|
||||
// yahoo.md: yahoo.com, yahoo.de, yahoo.it, yahoo.fr, yahoo.es, yahoo.se, yahoo.co.uk, yahoo.co.nz, yahoo.com.au, yahoo.com.ar, yahoo.com.br, yahoo.com.mx, ymail.com, rocketmail.com, yahoodns.net
|
||||
static ref P_YAHOO: Provider = Provider {
|
||||
status: Status::PREPARATION,
|
||||
before_login_hint: "To use Delta Chat with your Yahoo email address you have to allow \"less secure apps\" in the Yahoo webinterface.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/yahoo",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.mail.yahoo.com", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: SSL, hostname: "smtp.mail.yahoo.com", port: 465, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
// yandex.ru.md: yandex.ru, yandex.com
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
// ziggo.nl.md: ziggo.nl
|
||||
static ref P_ZIGGO_NL: Provider = Provider {
|
||||
status: Status::OK,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/ziggo-nl",
|
||||
server: vec![
|
||||
Server { protocol: IMAP, socket: SSL, hostname: "imap.ziggo.nl", port: 993, username_pattern: EMAIL },
|
||||
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.ziggo.nl", port: 587, username_pattern: EMAIL },
|
||||
],
|
||||
};
|
||||
|
||||
pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [
|
||||
("aktivix.org", &*P_AKTIVIX_ORG),
|
||||
("aol.com", &*P_AOL),
|
||||
("autistici.org", &*P_AUTISTICI_ORG),
|
||||
("bluewin.ch", &*P_BLUEWIN_CH),
|
||||
("example.com", &*P_EXAMPLE_COM),
|
||||
("example.org", &*P_EXAMPLE_COM),
|
||||
("fastmail.com", &*P_FASTMAIL),
|
||||
("freenet.de", &*P_FREENET_DE),
|
||||
("gmail.com", &*P_GMAIL),
|
||||
("googlemail.com", &*P_GMAIL),
|
||||
("gmx.net", &*P_GMX_NET),
|
||||
("gmx.de", &*P_GMX_NET),
|
||||
("gmx.at", &*P_GMX_NET),
|
||||
("gmx.ch", &*P_GMX_NET),
|
||||
("gmx.org", &*P_GMX_NET),
|
||||
("gmx.eu", &*P_GMX_NET),
|
||||
("gmx.info", &*P_GMX_NET),
|
||||
("gmx.biz", &*P_GMX_NET),
|
||||
("gmx.com", &*P_GMX_NET),
|
||||
("icloud.com", &*P_ICLOUD),
|
||||
("me.com", &*P_ICLOUD),
|
||||
("mac.com", &*P_ICLOUD),
|
||||
("nauta.cu", &*P_NAUTA_CU),
|
||||
("hotmail.com", &*P_OUTLOOK_COM),
|
||||
("outlook.com", &*P_OUTLOOK_COM),
|
||||
("office365.com", &*P_OUTLOOK_COM),
|
||||
("outlook.com.tr", &*P_OUTLOOK_COM),
|
||||
("live.com", &*P_OUTLOOK_COM),
|
||||
("posteo.de", &*P_POSTEO),
|
||||
("protonmail.com", &*P_PROTONMAIL),
|
||||
("protonmail.ch", &*P_PROTONMAIL),
|
||||
("t-online.de", &*P_T_ONLINE),
|
||||
("magenta.de", &*P_T_ONLINE),
|
||||
("testrun.org", &*P_TESTRUN),
|
||||
("tiscali.it", &*P_TISCALI_IT),
|
||||
("web.de", &*P_WEB_DE),
|
||||
("email.de", &*P_WEB_DE),
|
||||
("flirt.ms", &*P_WEB_DE),
|
||||
("hallo.ms", &*P_WEB_DE),
|
||||
("kuss.ms", &*P_WEB_DE),
|
||||
("love.ms", &*P_WEB_DE),
|
||||
("magic.ms", &*P_WEB_DE),
|
||||
("singles.ms", &*P_WEB_DE),
|
||||
("cool.ms", &*P_WEB_DE),
|
||||
("kanzler.ms", &*P_WEB_DE),
|
||||
("okay.ms", &*P_WEB_DE),
|
||||
("party.ms", &*P_WEB_DE),
|
||||
("pop.ms", &*P_WEB_DE),
|
||||
("stars.ms", &*P_WEB_DE),
|
||||
("techno.ms", &*P_WEB_DE),
|
||||
("clever.ms", &*P_WEB_DE),
|
||||
("deutschland.ms", &*P_WEB_DE),
|
||||
("genial.ms", &*P_WEB_DE),
|
||||
("ich.ms", &*P_WEB_DE),
|
||||
("online.ms", &*P_WEB_DE),
|
||||
("smart.ms", &*P_WEB_DE),
|
||||
("wichtig.ms", &*P_WEB_DE),
|
||||
("action.ms", &*P_WEB_DE),
|
||||
("fussball.ms", &*P_WEB_DE),
|
||||
("joker.ms", &*P_WEB_DE),
|
||||
("planet.ms", &*P_WEB_DE),
|
||||
("power.ms", &*P_WEB_DE),
|
||||
("yahoo.com", &*P_YAHOO),
|
||||
("yahoo.de", &*P_YAHOO),
|
||||
("yahoo.it", &*P_YAHOO),
|
||||
("yahoo.fr", &*P_YAHOO),
|
||||
("yahoo.es", &*P_YAHOO),
|
||||
("yahoo.se", &*P_YAHOO),
|
||||
("yahoo.co.uk", &*P_YAHOO),
|
||||
("yahoo.co.nz", &*P_YAHOO),
|
||||
("yahoo.com.au", &*P_YAHOO),
|
||||
("yahoo.com.ar", &*P_YAHOO),
|
||||
("yahoo.com.br", &*P_YAHOO),
|
||||
("yahoo.com.mx", &*P_YAHOO),
|
||||
("ymail.com", &*P_YAHOO),
|
||||
("rocketmail.com", &*P_YAHOO),
|
||||
("yahoodns.net", &*P_YAHOO),
|
||||
("ziggo.nl", &*P_ZIGGO_NL),
|
||||
].iter().copied().collect();
|
||||
}
|
||||
146
src/provider/mod.rs
Normal file
146
src/provider/mod.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
//! [Provider database](https://providers.delta.chat/) module
|
||||
|
||||
mod data;
|
||||
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::provider::data::PROVIDER_DATA;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum Status {
|
||||
OK = 1,
|
||||
PREPARATION = 2,
|
||||
BROKEN = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum Protocol {
|
||||
SMTP = 1,
|
||||
IMAP = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum Socket {
|
||||
STARTTLS = 1,
|
||||
SSL = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum UsernamePattern {
|
||||
EMAIL = 1,
|
||||
EMAILLOCALPART = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Server {
|
||||
pub protocol: Protocol,
|
||||
pub socket: Socket,
|
||||
pub hostname: &'static str,
|
||||
pub port: u16,
|
||||
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 Provider {
|
||||
pub status: Status,
|
||||
pub before_login_hint: &'static str,
|
||||
pub after_login_hint: &'static str,
|
||||
pub overview_page: &'static str,
|
||||
pub server: Vec<Server>,
|
||||
}
|
||||
|
||||
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,
|
||||
Err(_err) => return None,
|
||||
}
|
||||
.to_lowercase();
|
||||
|
||||
if let Some(provider) = PROVIDER_DATA.get(domain.as_str()) {
|
||||
return Some(*provider);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_provider_info_unexistant() {
|
||||
let provider = get_provider_info("user@unexistant.org");
|
||||
assert!(provider.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_provider_info_mixed_case() {
|
||||
let provider = get_provider_info("uSer@nAUta.Cu").unwrap();
|
||||
assert!(provider.status == Status::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_provider_info() {
|
||||
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();
|
||||
assert!(provider.status == Status::OK);
|
||||
let server = provider.get_imap_server().unwrap();
|
||||
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();
|
||||
assert_eq!(server.protocol, Protocol::SMTP);
|
||||
assert_eq!(server.socket, Socket::STARTTLS);
|
||||
assert_eq!(server.hostname, "smtp.nauta.cu");
|
||||
assert_eq!(server.port, 25);
|
||||
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
|
||||
|
||||
let provider = get_provider_info("user@gmail.com").unwrap();
|
||||
assert!(provider.status == Status::PREPARATION);
|
||||
assert!(!provider.before_login_hint.is_empty());
|
||||
assert!(!provider.overview_page.is_empty());
|
||||
|
||||
let provider = get_provider_info("user@googlemail.com").unwrap();
|
||||
assert!(provider.status == Status::PREPARATION);
|
||||
}
|
||||
}
|
||||
148
src/provider/update.py
Executable file
148
src/provider/update.py
Executable file
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
# if the yaml import fails, run "pip install pyyaml"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import yaml
|
||||
|
||||
out_all = ""
|
||||
out_domains = ""
|
||||
domains_dict = {}
|
||||
|
||||
|
||||
def cleanstr(s):
|
||||
s = s.strip()
|
||||
s = s.replace("\n", " ")
|
||||
s = s.replace("\\", "\\\\")
|
||||
s = s.replace("\"", "\\\"")
|
||||
return s
|
||||
|
||||
|
||||
def file2varname(f):
|
||||
f = f[f.rindex("/")+1:].replace(".md", "")
|
||||
f = f.replace(".", "_")
|
||||
f = f.replace("-", "_")
|
||||
return "P_" + f.upper()
|
||||
|
||||
|
||||
def file2url(f):
|
||||
f = f[f.rindex("/")+1:].replace(".md", "")
|
||||
f = f.replace(".", "-")
|
||||
return "https://providers.delta.chat/" + f
|
||||
|
||||
|
||||
def process_data(data, file):
|
||||
status = data.get("status", "")
|
||||
if status != "OK" and status != "PREPARATION" and status != "BROKEN":
|
||||
raise TypeError("bad status")
|
||||
|
||||
comment = ""
|
||||
domains = ""
|
||||
if not "domains" in data:
|
||||
raise TypeError("no domains found")
|
||||
for domain in data["domains"]:
|
||||
domain = cleanstr(domain)
|
||||
if domain == "" or domain.count(".") < 1 or domain.lower() != domain:
|
||||
raise TypeError("bad domain: " + domain)
|
||||
|
||||
global domains_dict
|
||||
if domains_dict.get(domain, False):
|
||||
raise TypeError("domain used twice: " + domain)
|
||||
domains_dict[domain] = True
|
||||
|
||||
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
|
||||
comment += domain + ", "
|
||||
|
||||
|
||||
server = ""
|
||||
has_imap = False
|
||||
has_smtp = False
|
||||
if "server" in data:
|
||||
for s in data["server"]:
|
||||
hostname = cleanstr(s.get("hostname", ""))
|
||||
port = int(s.get("port", ""))
|
||||
if hostname == "" or hostname.count(".") < 1 or port <= 0:
|
||||
raise TypeError("bad hostname or port")
|
||||
|
||||
protocol = s.get("type", "").upper()
|
||||
if protocol == "IMAP":
|
||||
has_imap = True
|
||||
elif protocol == "SMTP":
|
||||
has_smtp = True
|
||||
else:
|
||||
raise TypeError("bad protocol")
|
||||
|
||||
socket = s.get("socket", "").upper()
|
||||
if socket != "STARTTLS" and socket != "SSL":
|
||||
raise TypeError("bad socket")
|
||||
|
||||
username_pattern = s.get("username_pattern", "EMAIL").upper()
|
||||
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
|
||||
raise TypeError("bad username pattern")
|
||||
|
||||
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
|
||||
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern + " },\n")
|
||||
|
||||
provider = ""
|
||||
before_login_hint = cleanstr(data.get("before_login_hint", ""))
|
||||
after_login_hint = cleanstr(data.get("after_login_hint", ""))
|
||||
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
|
||||
provider += " static ref " + file2varname(file) + ": Provider = Provider {\n"
|
||||
provider += " status: Status::" + status + ",\n"
|
||||
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
|
||||
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
|
||||
provider += " overview_page: \"" + file2url(file) + "\",\n"
|
||||
provider += " server: vec![\n" + server + " ],\n"
|
||||
provider += " };\n\n"
|
||||
else:
|
||||
raise TypeError("SMTP and IMAP must be specified together or left out both")
|
||||
|
||||
if status != "OK" and before_login_hint == "":
|
||||
raise TypeError("status PREPARATION or BROKEN requires before_login_hint: " + file)
|
||||
|
||||
# finally, add the provider
|
||||
global out_all, out_domains
|
||||
out_all += " // " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
|
||||
if status == "OK" and before_login_hint == "" and after_login_hint == "" and server == "":
|
||||
out_all += " // - skipping provider with status OK and no special things to do\n\n"
|
||||
else:
|
||||
out_all += provider
|
||||
out_domains += domains
|
||||
|
||||
|
||||
def process_file(file):
|
||||
print("processing file: " + file, file=sys.stderr)
|
||||
with open(file) as f:
|
||||
# load_all() loads "---"-separated yamls -
|
||||
# by coincidence, this is also the frontmatter separator :)
|
||||
data = next(yaml.load_all(f, Loader=yaml.SafeLoader))
|
||||
process_data(data, file)
|
||||
|
||||
|
||||
def process_dir(dir):
|
||||
print("processing directory: " + dir, file=sys.stderr)
|
||||
files = [f for f in os.listdir(dir) if f.endswith(".md")]
|
||||
files.sort()
|
||||
for f in files:
|
||||
process_file(os.path.join(dir, f))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
raise SystemExit("usage: update.py DIR_WITH_MD_FILES > data.rs")
|
||||
|
||||
out_all = ("// file generated by src/provider/update.py\n\n"
|
||||
"use crate::provider::Protocol::*;\n"
|
||||
"use crate::provider::Socket::*;\n"
|
||||
"use crate::provider::UsernamePattern::*;\n"
|
||||
"use crate::provider::*;\n"
|
||||
"use std::collections::HashMap;\n\n"
|
||||
"lazy_static::lazy_static! {\n\n")
|
||||
|
||||
process_dir(sys.argv[1])
|
||||
|
||||
out_all += " pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [\n"
|
||||
out_all += out_domains;
|
||||
out_all += " ].iter().copied().collect();\n}"
|
||||
|
||||
print(out_all)
|
||||
99
src/qr.rs
99
src/qr.rs
@@ -4,17 +4,21 @@ use lazy_static::lazy_static;
|
||||
use percent_encoding::percent_decode_str;
|
||||
|
||||
use crate::chat;
|
||||
use crate::config::*;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::error::Error;
|
||||
use crate::key::dc_format_fingerprint;
|
||||
use crate::key::*;
|
||||
use crate::key::dc_normalize_fingerprint;
|
||||
use crate::lot::{Lot, LotState};
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
use reqwest::Url;
|
||||
use serde::Deserialize;
|
||||
|
||||
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
|
||||
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
|
||||
const MAILTO_SCHEME: &str = "mailto:";
|
||||
const MATMSG_SCHEME: &str = "MATMSG:";
|
||||
const VCARD_SCHEME: &str = "BEGIN:VCARD";
|
||||
@@ -43,6 +47,8 @@ pub fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
|
||||
|
||||
if qr.starts_with(OPENPGP4FPR_SCHEME) {
|
||||
decode_openpgp(context, qr)
|
||||
} else if qr.starts_with(DCACCOUNT_SCHEME) {
|
||||
decode_account(context, qr)
|
||||
} else if qr.starts_with(MAILTO_SCHEME) {
|
||||
decode_mailto(context, qr)
|
||||
} else if qr.starts_with(SMTP_SCHEME) {
|
||||
@@ -179,6 +185,73 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
lot
|
||||
}
|
||||
|
||||
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
|
||||
fn decode_account(_context: &Context, qr: &str) -> Lot {
|
||||
let payload = &qr[DCACCOUNT_SCHEME.len()..];
|
||||
|
||||
let mut lot = Lot::new();
|
||||
|
||||
if let Ok(url) = Url::parse(payload) {
|
||||
if url.scheme() == "https" {
|
||||
lot.state = LotState::QrAccount;
|
||||
lot.text1 = url.host_str().map(|x| x.to_string());
|
||||
} else {
|
||||
lot.state = LotState::QrError;
|
||||
lot.text1 = Some(format!("Bad scheme for account url: {}", payload));
|
||||
}
|
||||
} else {
|
||||
lot.state = LotState::QrError;
|
||||
lot.text1 = Some(format!("Invalid account url: {}", payload));
|
||||
}
|
||||
|
||||
lot
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CreateAccountResponse {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
|
||||
/// download additional information from the contained url and set the parameters.
|
||||
/// on success, a configure::configure() should be able to log in to the account
|
||||
pub fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
|
||||
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
|
||||
|
||||
let response = reqwest::blocking::Client::new().post(url_str).send();
|
||||
if response.is_err() {
|
||||
return Err(format_err!(
|
||||
"Cannot create account, request to {} failed",
|
||||
url_str
|
||||
));
|
||||
}
|
||||
let response = response.unwrap();
|
||||
if !response.status().is_success() {
|
||||
return Err(format_err!(
|
||||
"Request to {} unsuccessful: {:?}",
|
||||
url_str,
|
||||
response
|
||||
));
|
||||
}
|
||||
|
||||
let parsed: reqwest::Result<CreateAccountResponse> = response.json();
|
||||
if parsed.is_err() {
|
||||
return Err(format_err!(
|
||||
"Failed to parse JSON response from {}: error: {:?}",
|
||||
url_str,
|
||||
parsed
|
||||
));
|
||||
}
|
||||
println!("response: {:?}", &parsed);
|
||||
let parsed = parsed.unwrap();
|
||||
|
||||
context.set_config(Config::Addr, Some(&parsed.email))?;
|
||||
context.set_config(Config::MailPw, Some(&parsed.password))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract address for the mailto scheme.
|
||||
///
|
||||
/// Scheme: `mailto:addr...?subject=...&body=..`
|
||||
@@ -493,4 +566,28 @@ mod tests {
|
||||
assert_eq!(res.get_state(), LotState::QrError);
|
||||
assert_eq!(res.get_id(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_account() {
|
||||
let ctx = dummy_context();
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
);
|
||||
assert_eq!(res.get_state(), LotState::QrAccount);
|
||||
assert_eq!(res.get_text1().unwrap(), "example.org");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_account_bad_scheme() {
|
||||
let ctx = dummy_context();
|
||||
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
);
|
||||
assert_eq!(res.get_state(), LotState::QrError);
|
||||
assert!(res.get_text1().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::e2ee::*;
|
||||
use crate::error::Error;
|
||||
use crate::events::Event;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::*;
|
||||
use crate::key::{dc_normalize_fingerprint, Key};
|
||||
use crate::lot::LotState;
|
||||
use crate::message::Message;
|
||||
use crate::mimeparser::*;
|
||||
@@ -143,6 +143,8 @@ fn get_self_fingerprint(context: &Context) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group handshake.
|
||||
/// See the ffi-documentation for more details.
|
||||
pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
let cleanup =
|
||||
|context: &Context, contact_chat_id: ChatId, ongoing_allocated: bool, join_vg: bool| {
|
||||
@@ -257,11 +259,18 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
);
|
||||
}
|
||||
|
||||
while !context.shall_stop_ongoing() {
|
||||
// Don't sleep too long, the user is waiting.
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
if join_vg {
|
||||
// for a group-join, wait until the secure-join is done and the group is created
|
||||
while !context.shall_stop_ongoing() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
}
|
||||
cleanup(&context, contact_chat_id, true, join_vg)
|
||||
} else {
|
||||
// for a one-to-one-chat, the chat is already known, return the chat-id,
|
||||
// the verification runs in background
|
||||
context.free_ongoing();
|
||||
contact_chat_id
|
||||
}
|
||||
cleanup(&context, contact_chat_id, true, join_vg)
|
||||
}
|
||||
|
||||
fn send_handshake_msg(
|
||||
@@ -402,7 +411,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not) {
|
||||
Ok((chat_id, blocked)) => {
|
||||
if blocked != Blocked::Not {
|
||||
chat::unblock(context, chat_id);
|
||||
chat_id.unblock(context);
|
||||
}
|
||||
chat_id
|
||||
}
|
||||
@@ -476,7 +485,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
|
||||
let auth = get_qr_attr!(context, auth).to_string();
|
||||
|
||||
if !encrypted_and_signed(mime_message, &scanned_fingerprint_of_alice) {
|
||||
if !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
@@ -539,7 +548,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
if !encrypted_and_signed(mime_message, &fingerprint) {
|
||||
if !encrypted_and_signed(context, mime_message, &fingerprint) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
@@ -623,22 +632,27 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
==== Bob - the joiner's side ====
|
||||
==== Step 7 in "Setup verified contact" protocol ====
|
||||
=======================================================*/
|
||||
let abort_retval = if join_vg {
|
||||
HandshakeMessage::Propagate
|
||||
} else {
|
||||
HandshakeMessage::Ignore
|
||||
};
|
||||
|
||||
if context.bob.read().unwrap().expects != DC_VC_CONTACT_CONFIRM {
|
||||
info!(context, "Message belongs to a different handshake.",);
|
||||
return Ok(HandshakeMessage::Propagate);
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
let cond = {
|
||||
let bob = context.bob.read().unwrap();
|
||||
let scan = bob.qr_scan.as_ref();
|
||||
scan.is_none() || join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup
|
||||
scan.is_none() || (join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup)
|
||||
};
|
||||
if cond {
|
||||
warn!(
|
||||
context,
|
||||
"Message out of sync or belongs to a different handshake.",
|
||||
);
|
||||
return Ok(HandshakeMessage::Propagate);
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
|
||||
|
||||
@@ -661,7 +675,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
true
|
||||
};
|
||||
if vg_expect_encrypted
|
||||
&& !encrypted_and_signed(mime_message, &scanned_fingerprint_of_alice)
|
||||
&& !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice)
|
||||
{
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
@@ -669,7 +683,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
"Contact confirm message not encrypted.",
|
||||
);
|
||||
context.bob.write().unwrap().status = 0;
|
||||
return Ok(HandshakeMessage::Propagate);
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
|
||||
if mark_peer_as_verified(context, &scanned_fingerprint_of_alice).is_err() {
|
||||
@@ -678,7 +692,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on joiner-side.",
|
||||
);
|
||||
return Ok(HandshakeMessage::Propagate);
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined);
|
||||
emit_event!(context, Event::ContactsChanged(None));
|
||||
@@ -692,7 +706,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
.map_err(|_| HandshakeError::NoSelfAddr)?
|
||||
{
|
||||
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
|
||||
return Ok(HandshakeMessage::Propagate);
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
secure_connection_established(context, contact_chat_id);
|
||||
context.bob.write().unwrap().expects = 0;
|
||||
@@ -709,7 +723,11 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
}
|
||||
context.bob.write().unwrap().status = 1;
|
||||
context.stop_ongoing();
|
||||
Ok(HandshakeMessage::Propagate)
|
||||
Ok(if join_vg {
|
||||
HandshakeMessage::Propagate
|
||||
} else {
|
||||
HandshakeMessage::Done
|
||||
})
|
||||
}
|
||||
"vg-member-added-received" => {
|
||||
/*==========================================================
|
||||
@@ -752,6 +770,38 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
}
|
||||
}
|
||||
|
||||
/// observe_securejoin_on_other_device() must be called when a self-sent securejoin message is seen.
|
||||
/// currently, the message is only ignored, in the future,
|
||||
/// we may mark peers as verified accross devices:
|
||||
///
|
||||
/// in a multi-device-setup, there may be other devices that "see" the handshake messages.
|
||||
/// if the seen messages seen are self-sent messages encrypted+signed correctly with our key,
|
||||
/// we can make some conclusions of it:
|
||||
///
|
||||
/// - if we see the self-sent-message vg-member-added/vc-contact-confirm,
|
||||
/// we know that we're an inviter-observer.
|
||||
/// the inviting device has marked a peer as verified on vg-request-with-auth/vc-request-with-auth
|
||||
/// before sending vg-member-added/vc-contact-confirm - so, if we observe vg-member-added/vc-contact-confirm,
|
||||
/// we can mark the peer as verified as well.
|
||||
///
|
||||
/// - if we see the self-sent-message vg-member-added-received
|
||||
/// we know that we're an joiner-observer.
|
||||
/// the joining device has marked the peer as verified on vg-member-added/vc-contact-confirm
|
||||
/// before sending vg-member-added-received - so, if we observe vg-member-added-received,
|
||||
/// we can mark the peer as verified as well.
|
||||
///
|
||||
/// to make this work, (a) some messages must not be deleted,
|
||||
/// (b) we need a vc-contact-confirm-received message if bcc_self is set,
|
||||
/// (c) we should make sure, we do not only rely on the unencrypted To:-header for identifying the peer
|
||||
/// (in handle_securejoin_handshake() we have the oob information for that)
|
||||
pub(crate) fn observe_securejoin_on_other_device(
|
||||
_context: &Context,
|
||||
_mime_message: &MimeMessage,
|
||||
_contact_id: u32,
|
||||
) -> Result<HandshakeMessage, HandshakeError> {
|
||||
Ok(HandshakeMessage::Ignore)
|
||||
}
|
||||
|
||||
fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
|
||||
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id);
|
||||
let contact = Contact::get_by_id(context, contact_id);
|
||||
@@ -812,22 +862,26 @@ fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Res
|
||||
* Tools: Misc.
|
||||
******************************************************************************/
|
||||
|
||||
fn encrypted_and_signed(mimeparser: &MimeMessage, expected_fingerprint: impl AsRef<str>) -> bool {
|
||||
fn encrypted_and_signed(
|
||||
context: &Context,
|
||||
mimeparser: &MimeMessage,
|
||||
expected_fingerprint: impl AsRef<str>,
|
||||
) -> bool {
|
||||
if !mimeparser.was_encrypted() {
|
||||
warn!(mimeparser.context, "Message not encrypted.",);
|
||||
warn!(context, "Message not encrypted.",);
|
||||
false
|
||||
} else if mimeparser.signatures.is_empty() {
|
||||
warn!(mimeparser.context, "Message not signed.",);
|
||||
warn!(context, "Message not signed.",);
|
||||
false
|
||||
} else if expected_fingerprint.as_ref().is_empty() {
|
||||
warn!(mimeparser.context, "Fingerprint for comparison missing.",);
|
||||
warn!(context, "Fingerprint for comparison missing.",);
|
||||
false
|
||||
} else if !mimeparser
|
||||
.signatures
|
||||
.contains(expected_fingerprint.as_ref())
|
||||
{
|
||||
warn!(
|
||||
mimeparser.context,
|
||||
context,
|
||||
"Message does not match expected fingerprint {}.",
|
||||
expected_fingerprint.as_ref(),
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
pub mod send;
|
||||
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use async_smtp::smtp::client::net::*;
|
||||
use async_smtp::*;
|
||||
@@ -38,11 +38,11 @@ pub enum Error {
|
||||
Oauth2Error { address: String },
|
||||
|
||||
#[fail(display = "TLS error")]
|
||||
Tls(#[cause] native_tls::Error),
|
||||
Tls(#[cause] async_native_tls::Error),
|
||||
}
|
||||
|
||||
impl From<native_tls::Error> for Error {
|
||||
fn from(err: native_tls::Error) -> Error {
|
||||
impl From<async_native_tls::Error> for Error {
|
||||
fn from(err: async_native_tls::Error) -> Error {
|
||||
Error::Tls(err)
|
||||
}
|
||||
}
|
||||
@@ -53,8 +53,14 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub struct Smtp {
|
||||
#[debug_stub(some = "SmtpTransport")]
|
||||
transport: Option<smtp::SmtpTransport>,
|
||||
|
||||
/// Email address we are sending from.
|
||||
from: Option<EmailAddress>,
|
||||
|
||||
/// Timestamp of last successful send/receive network interaction
|
||||
/// (eg connect or send succeeded). On initialization and disconnect
|
||||
/// it is set to None.
|
||||
last_success: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Smtp {
|
||||
@@ -68,6 +74,17 @@ impl Smtp {
|
||||
if let Some(mut transport) = self.transport.take() {
|
||||
async_std::task::block_on(transport.close()).ok();
|
||||
}
|
||||
self.last_success = None;
|
||||
}
|
||||
|
||||
/// Return true if smtp was connected but is not known to
|
||||
/// have been successfully used the last 60 seconds
|
||||
pub fn has_maybe_stale_connection(&self) -> bool {
|
||||
if let Some(last_success) = self.last_success {
|
||||
Instant::now().duration_since(last_success).as_secs() > 60
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether we are connected.
|
||||
@@ -104,7 +121,7 @@ impl Smtp {
|
||||
let domain = &lp.send_server;
|
||||
let port = lp.send_port as u16;
|
||||
|
||||
let tls_config = dc_build_tls(lp.smtp_certificate_checks)?.into();
|
||||
let tls_config = dc_build_tls(lp.smtp_certificate_checks);
|
||||
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls_config);
|
||||
|
||||
let (creds, mechanism) = if 0 != lp.server_flags & (DC_LP_AUTH_OAUTH2 as i32) {
|
||||
@@ -161,6 +178,7 @@ impl Smtp {
|
||||
trans.connect().await.map_err(Error::ConnectionFailure)?;
|
||||
|
||||
self.transport = Some(trans);
|
||||
self.last_success = Some(Instant::now());
|
||||
context.call_cb(Event::SmtpConnected(format!(
|
||||
"SMTP-LOGIN as {} ok",
|
||||
lp.send_user,
|
||||
|
||||
@@ -53,6 +53,8 @@ impl Smtp {
|
||||
"Message len={} was smtp-sent to {}",
|
||||
message_len, recipients_display
|
||||
)));
|
||||
self.last_success = Some(std::time::Instant::now());
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
warn!(
|
||||
|
||||
71
src/sql.rs
71
src/sql.rs
@@ -8,7 +8,7 @@ use rusqlite::{Connection, OpenFlags, Statement, NO_PARAMS};
|
||||
use thread_local_object::ThreadLocal;
|
||||
|
||||
use crate::chat::{update_device_icon, update_saved_messages_icon};
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::constants::{ShowEmails, DC_CHAT_ID_TRASH};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::param::*;
|
||||
@@ -113,16 +113,16 @@ impl Sql {
|
||||
self.with_conn(|conn| conn.execute(sql, params).map_err(Into::into))
|
||||
}
|
||||
|
||||
fn with_conn<T, G>(&self, g: G) -> Result<T>
|
||||
pub fn with_conn<T, G>(&self, g: G) -> Result<T>
|
||||
where
|
||||
G: FnOnce(&Connection) -> Result<T>,
|
||||
G: FnOnce(&mut Connection) -> Result<T>,
|
||||
{
|
||||
let res = match &*self.pool.read().unwrap() {
|
||||
Some(pool) => {
|
||||
let conn = pool.get()?;
|
||||
let mut conn = pool.get()?;
|
||||
|
||||
// Only one process can make changes to the database at one time.
|
||||
// busy_timeout defines, that if a seconds process wants write access,
|
||||
// busy_timeout defines, that if a second process wants write access,
|
||||
// this second process will wait some milliseconds
|
||||
// and try over until it gets write access or the given timeout is elapsed.
|
||||
// If the second process does not get write access within the given timeout,
|
||||
@@ -130,7 +130,7 @@ impl Sql {
|
||||
// (without a busy_timeout, sqlite3_step() would return SQLITE_BUSY _at once_)
|
||||
conn.busy_timeout(Duration::from_secs(10))?;
|
||||
|
||||
g(&conn)
|
||||
g(&mut conn)
|
||||
}
|
||||
None => Err(Error::SqlNoConnection),
|
||||
};
|
||||
@@ -217,20 +217,37 @@ impl Sql {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn query_get_value<P, T>(&self, context: &Context, query: &str, params: P) -> Option<T>
|
||||
/// Executes a query which is expected to return one row and one
|
||||
/// column. If the query does not return a value or returns SQL
|
||||
/// `NULL`, returns `Ok(None)`.
|
||||
pub fn query_get_value_result<P, T>(&self, query: &str, params: P) -> Result<Option<T>>
|
||||
where
|
||||
P: IntoIterator,
|
||||
P::Item: rusqlite::ToSql,
|
||||
T: rusqlite::types::FromSql,
|
||||
{
|
||||
match self.query_row(query, params, |row| row.get::<_, T>(0)) {
|
||||
Ok(res) => Some(res),
|
||||
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => None,
|
||||
Ok(res) => Ok(Some(res)),
|
||||
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
|
||||
Err(Error::Sql(rusqlite::Error::InvalidColumnType(
|
||||
_,
|
||||
_,
|
||||
rusqlite::types::Type::Null,
|
||||
))) => None,
|
||||
))) => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Not resultified version of `query_get_value_result`. Returns
|
||||
/// `None` on error.
|
||||
pub fn query_get_value<P, T>(&self, context: &Context, query: &str, params: P) -> Option<T>
|
||||
where
|
||||
P: IntoIterator,
|
||||
P::Item: rusqlite::ToSql,
|
||||
T: rusqlite::types::FromSql,
|
||||
{
|
||||
match self.query_get_value_result(query, params) {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
error!(context, "sql: Failed query_row: {}", err);
|
||||
None
|
||||
@@ -343,7 +360,7 @@ impl Sql {
|
||||
.and_then(|r| r.parse().ok())
|
||||
}
|
||||
|
||||
fn start_stmt(&self, stmt: impl AsRef<str>) {
|
||||
pub fn start_stmt(&self, stmt: impl AsRef<str>) {
|
||||
if let Some(query) = self.in_use.get_cloned() {
|
||||
let bt = backtrace::Backtrace::new();
|
||||
eprintln!("old query: {}", query);
|
||||
@@ -868,6 +885,19 @@ fn open(
|
||||
update_icons = true;
|
||||
sql.set_raw_config_int(context, "dbversion", 61)?;
|
||||
}
|
||||
if dbversion < 62 {
|
||||
info!(context, "[migration] v62");
|
||||
sql.execute(
|
||||
"ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;",
|
||||
NO_PARAMS,
|
||||
)?;
|
||||
sql.set_raw_config_int(context, "dbversion", 62)?;
|
||||
}
|
||||
if dbversion < 63 {
|
||||
info!(context, "[migration] v63");
|
||||
sql.execute("UPDATE chats SET grpid='' WHERE type=100", NO_PARAMS)?;
|
||||
sql.set_raw_config_int(context, "dbversion", 63)?;
|
||||
}
|
||||
|
||||
// (2) updates that require high-level objects
|
||||
// (the structure is complete now and all objects are usable)
|
||||
@@ -1023,6 +1053,18 @@ pub fn get_rowid2_with_conn(
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes from the database locally deleted messages that also don't
|
||||
/// have a server UID.
|
||||
fn prune_tombstones(context: &Context) -> Result<()> {
|
||||
context.sql.execute(
|
||||
"DELETE FROM msgs \
|
||||
WHERE (chat_id = ? OR hidden) \
|
||||
AND server_uid = 0",
|
||||
params![DC_CHAT_ID_TRASH],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn housekeeping(context: &Context) {
|
||||
let mut files_in_use = HashSet::new();
|
||||
let mut unreferenced_count = 0;
|
||||
@@ -1135,6 +1177,13 @@ pub fn housekeeping(context: &Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = prune_tombstones(context) {
|
||||
warn!(
|
||||
context,
|
||||
"Houskeeping: Cannot prune message tombstones: {}", err
|
||||
);
|
||||
}
|
||||
|
||||
info!(context, "Housekeeping done.",);
|
||||
}
|
||||
|
||||
|
||||
@@ -547,8 +547,8 @@ mod tests {
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 2);
|
||||
|
||||
chat::delete(&t.ctx, chats.get_chat_id(0)).ok();
|
||||
chat::delete(&t.ctx, chats.get_chat_id(1)).ok();
|
||||
chats.get_chat_id(0).delete(&t.ctx).ok();
|
||||
chats.get_chat_id(1).delete(&t.ctx).ok();
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::KeyType;
|
||||
use crate::context::{Context, ContextCallback};
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::events::Event;
|
||||
use crate::key;
|
||||
use crate::key::{self, DcKey};
|
||||
|
||||
/// A Context and temporary directory.
|
||||
///
|
||||
/// The temporary directory can be used to store the SQLite database,
|
||||
/// see e.g. [test_context] which does this.
|
||||
pub struct TestContext {
|
||||
pub(crate) struct TestContext {
|
||||
pub ctx: Context,
|
||||
pub dir: TempDir,
|
||||
}
|
||||
@@ -25,7 +25,7 @@ pub struct TestContext {
|
||||
/// "db.sqlite" in the [TestContext.dir] directory.
|
||||
///
|
||||
/// [Context]: crate::context::Context
|
||||
pub fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext {
|
||||
pub(crate) fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let cb: Box<ContextCallback> = match callback {
|
||||
@@ -41,11 +41,11 @@ pub fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext {
|
||||
/// The context will be opened and use the SQLite database as
|
||||
/// specified in [test_context] but there is no callback hooked up,
|
||||
/// i.e. [Context::call_cb] will always return `0`.
|
||||
pub fn dummy_context() -> TestContext {
|
||||
pub(crate) fn dummy_context() -> TestContext {
|
||||
test_context(None)
|
||||
}
|
||||
|
||||
pub fn logging_cb(_ctx: &Context, evt: Event) {
|
||||
pub(crate) fn logging_cb(_ctx: &Context, evt: Event) {
|
||||
match evt {
|
||||
Event::Info(msg) => println!("I: {}", msg),
|
||||
Event::Warning(msg) => println!("W: {}", msg),
|
||||
@@ -54,27 +54,50 @@ pub fn logging_cb(_ctx: &Context, evt: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a pre-generated keypair for alice@example.com from disk.
|
||||
///
|
||||
/// This saves CPU cycles by avoiding having to generate a key.
|
||||
///
|
||||
/// The keypair was created using the crate::key::tests::gen_key test.
|
||||
pub(crate) fn alice_keypair() -> key::KeyPair {
|
||||
let addr = EmailAddress::new("alice@example.com").unwrap();
|
||||
let public =
|
||||
key::SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
|
||||
.unwrap();
|
||||
let secret =
|
||||
key::SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
|
||||
.unwrap();
|
||||
key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
secret,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates Alice with a pre-generated keypair.
|
||||
///
|
||||
/// Returns the address of the keypair created (alice@example.org).
|
||||
pub fn configure_alice_keypair(ctx: &Context) -> String {
|
||||
let addr = String::from("alice@example.org");
|
||||
ctx.set_config(Config::ConfiguredAddr, Some(&addr)).unwrap();
|
||||
|
||||
// The keypair was created using:
|
||||
// let (public, private) = crate::pgp::dc_pgp_create_keypair("alice@example.com")
|
||||
// .unwrap();
|
||||
// println!("{}", public.to_base64(64));
|
||||
// println!("{}", private.to_base64(64));
|
||||
let public =
|
||||
key::Key::from_base64(include_str!("../test-data/key/public.asc"), KeyType::Public)
|
||||
.unwrap();
|
||||
let private = key::Key::from_base64(
|
||||
include_str!("../test-data/key/private.asc"),
|
||||
KeyType::Private,
|
||||
)
|
||||
.unwrap();
|
||||
let saved = key::dc_key_save_self_keypair(&ctx, &public, &private, &addr, true, &ctx.sql);
|
||||
assert_eq!(saved, true, "Failed to save Alice's key");
|
||||
addr
|
||||
/// Returns the address of the keypair created (alice@example.com).
|
||||
pub(crate) fn configure_alice_keypair(ctx: &Context) -> String {
|
||||
let keypair = alice_keypair();
|
||||
ctx.set_config(Config::ConfiguredAddr, Some(&keypair.addr.to_string()))
|
||||
.unwrap();
|
||||
key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default)
|
||||
.expect("Failed to save Alice's key");
|
||||
keypair.addr.to_string()
|
||||
}
|
||||
|
||||
/// Load a pre-generated keypair for bob@example.net from disk.
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub(crate) fn bob_keypair() -> key::KeyPair {
|
||||
let addr = EmailAddress::new("bob@example.net").unwrap();
|
||||
let public =
|
||||
key::SignedPublicKey::from_base64(include_str!("../test-data/key/bob-public.asc")).unwrap();
|
||||
let secret =
|
||||
key::SignedSecretKey::from_base64(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
||||
key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
secret,
|
||||
}
|
||||
}
|
||||
|
||||
1
test-data/key/alice-public.asc
Normal file
1
test-data/key/alice-public.asc
Normal file
@@ -0,0 +1 @@
|
||||
mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4MCyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDrRuI8A/8tEEXAA7g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeIeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg==
|
||||
1
test-data/key/alice-secret.asc
Normal file
1
test-data/key/alice-secret.asc
Normal file
@@ -0,0 +1 @@
|
||||
lFgEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5AAAQDMpCY4sD5/DUR0jRjGC5WstwShz1q+5Vofo5mY9+XRXRA3tBlBbGljZSA8YWxpY2VAZXhhbXBsZS5vcmc+iJAEExYIADgWIQQub6LLI7Uy1yhjS1hksI9hqe2UQwUCXlh13QIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBksI9hqe2UQxN6AP4uDAsgzE7I1qg2aEMF/wPxqEztv1dMCj4YyScv/JtqTAD5AYzpxjrcdkmaULyRk2vYfNOEWOog60biPAP/LRBFwAOcXQReWHXdEgorBgEEAZdVAQUBAQdABu3I1stkhQFPCp5bZbm1Vuu6xYsn6dNSa0Xul6stth0DAQgHAAD/X9y9I/JFBeArkgR3U363cWXXxMCWftS+BDwM9zE4PrgQb4h4BBgWCAAgFiEELm+iyyO1MtcoY0tYZLCPYantlEMFAl5Ydd0CGwwACgkQZLCPYantlEMujwEA5sTwaewZXArM2oK8d5aAmyqGNLcLqC9KVXe0Sb1eYXoBANe5wjJV+gHCjIyHaHpxKf8BOflAlvfmmCj6K+neOwwC
|
||||
1
test-data/key/bob-public.asc
Normal file
1
test-data/key/bob-public.asc
Normal file
@@ -0,0 +1 @@
|
||||
xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA5//PjAzbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOeJw9kohATSqUtsRO0pFJeDvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeTxc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1JzdKTcDWryrSkvmgFdUqJ7pJDk1HFTt+x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBame1BPsE1PA7VzeTSJR2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS5uZXQ+wsCJBBABCAAzAhkBBQJeMMduAhsDBAsJCAcGFQgJCgsCAxYCARYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSH9oIIALbpmicuVghM3CloiCgJhPEFLFMaQZRDV/KCVVtBcHAhw6d42q8T50mhs+W3Va5E37DN+wcenj8CgeGPQY3kPp2cnZruYtLhLkZ1+VEay5BQFUMb8kY21XrNTQQET8vc0L8cCLQ7RCgm1tGiFVp1nqbjmGUdoru90ksoufWfoqVPjNrW+9eHFvY/Z7PqchCdMnbKOJiwwv4E3NDTySZ1UVZnDztGy95Aa8OZ3cntvbq4JVi7S+N38rRPPPzpZKx+M4DUGfDAoaq7O/Xemyk1sP6C/NgQvS8rri54PgkMgKSS4TyyEzdM+fzeNYFPXFGTbgj4p0pSueQV7/JUfYHRfe3OwE0EXjDHVwEIAKIHgS2yI2niSCN1tqcbLvkhLrEJCVcpGxmA7asl1flwWYrGOBhNJE2sCuZqkofqw6qrgsQ4GFgUU5xmcBCqIZ49jRu+aY38lT4WDFHSbe/mGtaIhb2ZYK6zo9W7Y3r6ud8hbUKJTDfl9qEvJpX/Y0syMjwng8SZNTdYMWgAE4NwcgMgdU3dMA3RT6ePJ4vKs38hmXmInLyZce+GJzmo2tpZyP8viPS7JpqojoCPB3G5h9aHeakp1Y4XKQaExANeWCyBJEhNwtNEOVEpQ0txFYPyDrtxV5y5e79IUP418r/PHsnH6UnxXGzB6LfVbSeEyDyKEl+w0PrlNklySomTZFUAEQEAAcLAdgQYAQgAIAUCXjDHbgIbDBYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSHVCIIAIH694HkQLXRAJlXmi8K/xMVP96ywJovL/B5l4S/vk/iR4P1lYsF55A3Z2PK/iFtwAgVsppcBIPBlqSI0GPDMvEIxj7UFOQfQzVpDes29wG8grHJEJqI/4TlRjOacxTGaJ5fIMsLXJD6nLBuoN5Z6zm3LjqIyOx4ZGrwradPO95OMGT2Xll3YNzUqSWe33RJLqNQ5ea9I7+qvKnW5Z9Yt5nQwOo8yD+f5fql8904B3eAyLqxgkdLmngAWmYhc7KOaKdAsx7TXBAKsoeHk51OPk59u7EbX35HWD6snl/phJdUYDXiddyYN/n2ZY9g80ycle2JfgpfrQGlh7oJqgCjZuk=
|
||||
1
test-data/key/bob-secret.asc
Normal file
1
test-data/key/bob-secret.asc
Normal file
@@ -0,0 +1 @@
|
||||
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
|
||||
1
test-data/key/charlie-public.asc
Normal file
1
test-data/key/charlie-public.asc
Normal file
@@ -0,0 +1 @@
|
||||
xsBNBF48nc4BCADSWr9fE2K1FcE7eSW/Z0MOuzdozKmQJsrmkb7Abssd1yARZuf3+YYh5WqeKJrSTUFD+mJNUhqtodBqBxFH+JzITMG5qGcAdBwXaeJYFMquezLAuVZsZ2b+Njk9Fw3XF4cUZB58ItO/ViFgDi4r5lqsdiiMvGrLmQD2m2BOB8U52ibFhnSVvvYi6rlsZ1HfqB+efD8InPKfiMKDqu909fgVchGJ7OwtTKanaF3v+rzQvXsnVal1yfc0YsmOM0ekWDhIR5EoSg8pJlBHVc/yBrxQWq9h2e2PUntLK29/qp/k/xsQHN3BAj/kuQQPzMARcUUvxo9Aq8n4CMzoFmrK2G+bABEBAAHNFTxjaGFybGllQGV4YW1wbGUubmV0PsLAiQQQAQgAMwIZAQUCXjyd5AIbAwQLCQgHBhUICQoLAgMWAgEWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV7/tMCACSeaPalMt/EwsCGxnWNNEWPX7fMSiAZx1bbPYULIroEBVgmObOkDqoB6y9tWQB/HUq44ZIYdhdw7wlzZp9d81dtxGA+QcRkTy8fr/P2KmQhwjV9m22xBCGABPSpFG+/iONEJADEA8Oe3SogI/iPksepLs9Gg9Ix9Qb1ZMt6+GErE7u0aAamW03NQW7SlgLruAKhjKLjP1wxryK4h0Mdac6CuGZQH/G3yZX3OQPkE2BGjEefhzj0yEKOGhYM716uswqecjB8HoYh5RaDcE+Shcze/OhSQesqj0RxoCpeN3/6QGcC6DT70Qrqeri0RtA495SS3YTqb2p9U2WISOEJIdXzsBNBF48nc4BCADKwwUPt5jp4yIdVdVdLyXGuJS/pC6t5Q64QlcEzHH03eVJHYH42shyWPiTE7HU3giqLhnPXjYN8/piONBQ7dQqTkYJtrx0aUBAoQ9p0p6eMGoY4pMxrclwxnvGE+2YplFNzXkcqxkSPy4n5kOPeq55tVHkWG+gTblZdlT9sKM8Ksr8gF728eaWgt+TQCnhuwa9h9BVzLAZquaATm7PvPKamMd7jIVoISXrZKuPBVrSDbx+DElg0HKj8sOxh3lNz2rmRTGWDbURPhqvVyQ2tXryEVfMZRauR9B9YCYEeedRwDrOSME2LaoU483w0j2Vnb4GkOLpg8Wrw+fnIcM32GXxABEBAAHCwHYEGAEIACAFAl48neQCGwwWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV78HXCACyxjrXpr7GKqQOMJGvNKytuVf7gHUbwLq7oAXoM6PixPkfZIArH/EHnt0GaBWq2r08REj2IRQ8t/8zfkWa9L2RxITez3dRkOUjf49i+J9g3oyleJDZVrAhoU2Uzwb+40tTGaErGpD6m3GqIe5wE7gXFBAf0IwQmIjic64ULCE5j2qaWMxwfvKIDuSD/bN+mSqlQbmekeX6bud4rMoGIUnkQthNAQwBQHOjkPZvdjXiDGFxmpDlcJv5e9LYv8kb141JmYIon9iIGWF3L52+SbvYXFAoPi4qovsdgUXezTRoaiR8Mgft4KUZZIeXUhC7PRuut8PsbN2P0WCh1CjIJph9
|
||||
1
test-data/key/charlie-secret.asc
Normal file
1
test-data/key/charlie-secret.asc
Normal file
@@ -0,0 +1 @@
|
||||
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
|
||||
1
test-data/key/dom-public.asc
Normal file
1
test-data/key/dom-public.asc
Normal file
@@ -0,0 +1 @@
|
||||
xsBNBF48no8BCADwN4PxXewE4iP3HHn/FE5r0x8x8byo9mJIIEOrjL1k0JWIcz3KvC5evfZ3M/ZK2QPBLhFYOck0+gCAR/eH1zgDZeYXUB1CWUoCKlZ9jH1UbfzE34ZxxiwCZy2ZdMnKxDF/ezyXsVvoEhGQE1+8A9Xy/ZXlplUbyYVacgdK9fQlh50d8FOU+7eplUts/SzXx52T83tB98oS9QVgJ4qIC+fu0xMgqdH7e0ithSc/owfYkKjH+isbD4n9U8KSqb4zzf2Y2gz7h6jdM+PPuWqTSARbH83j7qG8q4IjC7cO3CgSryk3f/MLkquKD8z9ybvrYKXSF04RMEuhgBxXnE0vA3ZpABEBAAHNETxkb21AZXhhbXBsZS5uZXQ+wsCJBBABCAAzAhkBBQJePJ6rAhsDBAsJCAcGFQgJCgsCAxYCARYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaoXGQIAM6KRSMBLKOb0IlXqSCy1Ve9MF02fDNmGFw7xC5v2gKpdDZZJctRLxH3ZFgQmpcSIZ4P/A7XcYnlIxxGxwzyhMmas9eJ1w3sPbvX5vJ4ZYGzooZSFUQXgO6+bm7qQlf7gFcsuTkF5n6cx9tDPcyxy01b7eaexTXoWpYDX110TFbpklprub2QpI2w4gy5y1d/pfOmJYGHgMEALgAEKQCWjQ/jUS6pOVEH+pT1ddlv9vq37CmTp00bKbj+Z59mqeUBjE5+RmNWaLwkdZSR1o+LKNw/Gl07gzayRjJyh0Txj4LgHtVSKvGj6GexhddEgqp8qwMMfNbbqWATh9zHNJ+gjSzOwE0EXjyejwEIAKbISj1O986suBXKzHPlEzqetkGEWNV4OHHfhphIxsoW/f3IOdqv0uqwz9AhzXE6YbsFzrc6ZwP+pYTH51T1ugtG0LBMFikh4tyyYjJV0v9gUq2JqixxHfJ/XfVryKO1OWqs2GtBxaQP5FVf+vQqSPpAz4B/6hqwUvx2XdI6bbGn1fQlTQyfGR6Uclm5kIaY8/VBzuVySaAycfOdBhjoyYhl2eMxdxz4NCb6pwNkl6KbjSfGHVpfR6fbuTihIbZFL1qAfYi6BnvrB9HgnfYItIA9VV2SlLIRfBgxgG9vxKmD83j9r5Dn/e1KC5PoMndMZ5I410H06kjR6dLJbTuy7+MAEQEAAcLAdgQYAQgAIAUCXjyeqwIbDBYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaon5UIAMJLU7EKjCsJ4ldIYYRYE7wzVO2DpiFY5yEVu/Jq2O2C+go7b2oBgUYYB3ExwMUwPnU8H6j+jbzukxOltKeRZ1QU1d9iwzYVHsP3kw02DceoyMaab0j1DLHPSVPSmsP5/U0eFK++vFvsY8KuwRUPLK+m7+qFET2gidyRSJGqyiJlEz+wyR3b44Ff1C3RDdRx8EzPVYx+gXRGLIBqsBNuSLhimiwAekMgnpAdsQu8WTCPQSO2Yrz6I6uFVNAr8FnnFoeclwLHbQ++aWhBTuhUp9oJ/388ySkEuMAZrd4YU/RwthM4zsW/VAf936FBkvi8FBJQ0Vdps1xksQeXK7hQrVw=
|
||||
1
test-data/key/dom-secret.asc
Normal file
1
test-data/key/dom-secret.asc
Normal file
@@ -0,0 +1 @@
|
||||
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
|
||||
1
test-data/key/elena-public.asc
Normal file
1
test-data/key/elena-public.asc
Normal file
@@ -0,0 +1 @@
|
||||
xsBNBF48nxEBCADRa0HRyoj7KdbchkVycHq5jYxYN4NJsRf1bojhuifuLYlJQu7I7Rp862sbvSPN9tM/3dQF6fTyUllFkhoS50fUaf/bJWwi5XWVsLa1+DdOW4As2zkJslu6Hp/hUFM2FXthRYIwg3c2jparkNtsLyQvtDsG62Acg7dr+NwZ5mxepyJ5WkXciOPLp9egcrTVoBbcnn4gzj2Wx6MkUByY+bENtUrqsZerWOt+F75DIWkHwzo5VwfpvH6RrIJabu8KLMLoUTZoovQcDKfk0yv8bdBLEQf84SLY+0BCZ6aZHrqnwEmDBeFGfTcIMFsBxUZfvsMYlAiG6khbZzhQxJ44+YaxABEBAAHNEzxlbGVuYUBleGFtcGxlLm5ldD7CwIkEEAEIADMCGQEFAl48ny0CGwMECwkIBwYVCAkKCwIDFgIBFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noIpsAgAiP+E48xEhCvEHVIMiQigQvC3kucg2TRk0yrp0ydDeS19l8jXkN8lA9byXVq5VDg8Bs4tN9WR/Gy8x9Xmd6/VyFiquBqGTObO/1u1kegHR0+sp5FxxffZhGoCRBIW4iXww1BzcGUd+TeRlInl5adGpA6vQAGifLsKQQU9/I0bSRBAMGbo0PKZ+EllTLqDeo4D4ELNO6CKyh65FExUIZTtbWZTyLn95ekypUIhgEAKgEs/qE74nJBqcrzkXn3xmDJetYxw2xQzwS+rIwR12ONub01nm/74Z30zbMViwQF4yTV7VB9uklY6WayCBpT3BkFQiYRPt7lnipBI2PplYXvYIs7ATQRePJ8RAQgAwvvNWB4eABzpUylyhI7q8WNK8GHCGLfqprSLFiAv14psluRW9MezLEFM1N8Az5yqzs3hsuEMiIBPiLrkW8ZkCledlYENorM/6G5+xK9TI4iWnP1LP+qESGGNSF7pXciZE7/XrE/CPL06nXuJRd1qOHvIUnaCQRiqLFPkUG3KhtC+/Ayvz6l2RvSqoTTayxYckgigkEneS/RXMSYnG/A5RJB0fOACp39HzHN/XU7JFLz8WlXgLfWJi7v9uVft9QBCtVseWF+ElKJ5NkVNRrV/SQwFYB91Y+LHqKz6CTQq2Di0vGHjY9ecai16D/CGUqkJg9t5jnLxZvQeR70o13JqlQARAQABwsB2BBgBCAAgBQJePJ8uAhsMFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noLj2gf9EuLnhKUv0rWW2HShJlKF5oqKmx6tSoNMOd5hbJzL1Z9d7ZjFQJS25jAzl9V6858heAm3HYTlJfg2RkzHte12k0hr9Svbpadf/OYB2UVsAwGRFGbdq/3H/ZaP5EbCdv8Egcx6pjdgvXb5n6gaMj7LJG4/YvokGLOx3VgNNRdqB0gtKWwN0tzdM/6YfDJTEVDbMUR3aFqYLTSB8KCjNtWcnWO2a17P1Qf6fDhcxpyELkLb1T4sPD4Tz9bFPTJzL87uS4+Ba+Xq2YE7aWa6e+C2HMgH6OghDLqoGpeNy01N3XSikyoFNdEmPYY/RdmuJNSuuyptQHgzh1vIOV7AOoQ+Rw==
|
||||
1
test-data/key/elena-secret.asc
Normal file
1
test-data/key/elena-secret.asc
Normal file
@@ -0,0 +1 @@
|
||||
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
|
||||
1
test-data/key/fiona-public.asc
Normal file
1
test-data/key/fiona-public.asc
Normal file
@@ -0,0 +1 @@
|
||||
xsBNBF48n2MBCAC741/YKzU76pWNijBCYC+hGSKZvBdanm4eJzi/eKPevUypdW2GJzFFJxWjx2wIcV8cLwQvY2Z+ieJaPPwGbUr0nH2S9lmghkCCKjGxWcmrx3sQr7x9431KYOM6+/SAZNjHYjWwcwy2QhE6J7qfC74LjTF4pv+ZRgSZDC+O70NpTWdNdwi/0Kn9gdj5diSwJmnBxQBGp/tnZuu3XGZrgKXlGlPspRMF3Ug2WmvyBhxF8KKoosiaXlumc5gFFuFRnoAVfp/6yehO44l9bYz0zwYWahxmLhShQVhfcH3YBg0l7hMkfC2Fp8fDF7Hr1PUOJcBY50VDN0zlV9Bi1iiPofAtABEBAAHNEzxmaW9uYUBleGFtcGxlLm5ldD7CwIkEEAEIADMCGQEFAl48n3wCGwMECwkIBwYVCAkKCwIDFgIBFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZUGWQgApCPQYmfa76xcscYBWg3DvMAm0Exk/LvbN74MIqADHIgaNFUHoThTPDVAPeh5ogra/kg4QaLctivC81S2VOPIc/4LGEFJekvythXUgSLNjhlR63Va5ObMV4UegUH9c0O8MGndkQFxiwy98D1bzNyl3mCVOUECVZiJJbxUZwBKj4iowa7kgX/FrdjZIJrz50NMExcDuxvc+MGyDr4YQHrZTbCjcrJkxufklwsWme0qneuPTxwW2+TnDjHkDaYDdwEnoKRkwlXEy3Lu9ZhswlaWTgdtClpf2geb7tCAPQ0cd2V5m6NnKMzXrnFuJXB6Xkvp32Tyuq4ebILsbCfpLV/HzM7ATQRePJ9jAQgAzQGckmcFjCViSkwGLROhi2Gvb85ABXbkKPMg1x27LraP9YWNJoq2GOy2vv8r8m2q+FpCR25a0NdWlbyiQpPuEWh0udJnNUm+6j5j6PSAmlRRDMhDH4QwzJC+B+lM6NxvSRhxBBtwFAvMy0qeNhv1UCBaeQUHoFCODqJRYOywj7ZGXdQyttIAlC5PtVw1cV+J8/TGXNrE9DNgFGAm1BPgw/lE1OjVbF8l6NbDdi9YJoOm9mpffUPlUZMZh3+a5+J6F3KjYwNyi4wi3Y3Pt4avXEo+ib15XNhJcwMslc49La2T8PMnpf3nLClxynJjYDiMuzujcbBWbfVF1383P0KikQARAQABwsB2BBgBCAAgBQJePJ98AhsMFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZX1eAf9GnrA9nCds3OmtCUpRmVEDar29DfS79B4vBfoYXYb5kRIOxJV6yjfGYRh2IJ0CTfNYkp4AuRC/jEHPXlVUD92Vcb0wSfwO32mMw75FRzIS7/IcwWAauWOtpao3J/tsxHWkSxfh5Zo6vzODQY4k8eHnitxpXT0xSIjnYmVRMuqfb3NELk3PkF52+Fte4zKfBCP80HqO3BdVCBlQNpZiOMW+yFO/8VqLmB9442nGGhuSfCeBystI3Er0SYSDqNbg5uetBaP1MOomIZuuxhv3T5jD9DeEDHGOqDjPZ8dMdQYCnYmiVaMb8ECsKG5eMvS8Imss1ho2iz4sjYdVzODl8T0zQ==
|
||||
1
test-data/key/fiona-secret.asc
Normal file
1
test-data/key/fiona-secret.asc
Normal file
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user