mirror of
https://github.com/chatmail/core.git
synced 2026-04-07 08:02:11 +03:00
Compare commits
235 Commits
mailparse-
...
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 |
75
CHANGELOG.md
75
CHANGELOG.md
@@ -1,5 +1,80 @@
|
||||
# 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
|
||||
|
||||
1292
Cargo.lock
generated
1292
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.0.0-beta.24"
|
||||
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"
|
||||
@@ -53,11 +53,12 @@ bitflags = "1.1.0"
|
||||
debug_stub_derive = "0.3.0"
|
||||
sanitize-filename = "0.2.1"
|
||||
stop-token = { version = "0.1.1", features = ["unstable"] }
|
||||
mailparse = { git = "https://github.com/link2xt/mailparse", branch="address-comma" }
|
||||
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 }
|
||||
|
||||
@@ -84,7 +85,7 @@ required-features = ["rustyline"]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["nightly", "ringbuf"]
|
||||
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
|
||||
|
||||
@@ -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.24"
|
||||
version = "1.28.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -23,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().
|
||||
*
|
||||
@@ -895,6 +909,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
||||
#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
|
||||
|
||||
|
||||
/**
|
||||
@@ -918,7 +933,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
||||
* 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.
|
||||
@@ -933,6 +948,8 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
||||
* 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)
|
||||
@@ -1286,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.
|
||||
@@ -1372,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);
|
||||
|
||||
|
||||
/**
|
||||
@@ -1656,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()
|
||||
@@ -2846,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);
|
||||
|
||||
|
||||
/**
|
||||
@@ -3773,7 +3792,7 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
||||
* accessor functions. If no provider info is found, NULL will be
|
||||
* returned.
|
||||
*/
|
||||
dc_provider_t* dc_provider_new_from_email (const dc_context_t*, const char* email);
|
||||
dc_provider_t* dc_provider_new_from_email (const dc_context_t* context, const char* email);
|
||||
|
||||
|
||||
/**
|
||||
@@ -4132,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
|
||||
|
||||
|
||||
/**
|
||||
@@ -4532,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
|
||||
|
||||
|
||||
/*
|
||||
@@ -4541,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
|
||||
@@ -4597,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.
|
||||
*
|
||||
|
||||
@@ -25,8 +25,7 @@ use std::time::{Duration, SystemTime};
|
||||
use libc::uintptr_t;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::chat::MuteDuration;
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
@@ -86,17 +85,6 @@ pub type dc_callback_t =
|
||||
pub type dc_context_t = ContextWrapper;
|
||||
|
||||
impl ContextWrapper {
|
||||
/// Log an error 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()));
|
||||
}
|
||||
|
||||
/// Log a warning on the FFI context.
|
||||
///
|
||||
/// Like [error] but logs as a warning which only goes to the
|
||||
@@ -120,10 +108,6 @@ 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.
|
||||
///
|
||||
/// Prefer to use [ContextWrapper::try_inner], we might want to
|
||||
/// remove this function at some point to reduce the cognitive
|
||||
/// overload of having two functions which are too similar.
|
||||
unsafe fn with_inner<T, F>(&self, ctxfn: F) -> Result<T, ()>
|
||||
where
|
||||
F: FnOnce(&Context) -> T,
|
||||
@@ -367,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
|
||||
}
|
||||
}
|
||||
@@ -388,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()
|
||||
}
|
||||
}
|
||||
@@ -449,7 +433,7 @@ pub unsafe extern "C" fn dc_set_config_from_qr(
|
||||
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();
|
||||
@@ -504,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]
|
||||
@@ -517,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)
|
||||
}
|
||||
|
||||
@@ -739,7 +721,7 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default)?;
|
||||
Ok(1)
|
||||
})
|
||||
.log_warn(ffi_context, "Failed to save keypair")
|
||||
.log_err(ffi_context, "Failed to save keypair")
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -786,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)
|
||||
})
|
||||
@@ -806,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)
|
||||
})
|
||||
@@ -826,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)
|
||||
})
|
||||
@@ -1063,6 +1045,25 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
|
||||
.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)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_fresh_msgs(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1095,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(())
|
||||
@@ -1111,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(())
|
||||
@@ -1204,28 +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| {
|
||||
ChatId::new(chat_id)
|
||||
.set_archived(ctx, archive)
|
||||
.log_err(ctx, "Failed archive chat")
|
||||
.set_visibility(ctx, visibility)
|
||||
.log_err(ffi_context, "Failed setting chat visibility")
|
||||
.unwrap_or(())
|
||||
})
|
||||
.unwrap_or(())
|
||||
@@ -1242,7 +1247,7 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
|
||||
.with_inner(|ctx| {
|
||||
ChatId::new(chat_id)
|
||||
.delete(ctx)
|
||||
.log_err(ctx, "Failed chat delete")
|
||||
.log_err(ffi_context, "Failed chat delete")
|
||||
.unwrap_or(())
|
||||
})
|
||||
.unwrap_or(())
|
||||
@@ -1329,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)
|
||||
})
|
||||
@@ -1469,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
|
||||
@@ -2068,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();
|
||||
}
|
||||
|
||||
@@ -2418,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()
|
||||
@@ -2467,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]
|
||||
@@ -2736,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()
|
||||
@@ -2755,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())
|
||||
@@ -2766,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()
|
||||
@@ -2776,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3106,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()
|
||||
@@ -3116,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()
|
||||
@@ -3128,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()
|
||||
@@ -3140,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()
|
||||
@@ -3152,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()
|
||||
@@ -3235,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]
|
||||
@@ -3246,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]
|
||||
@@ -3298,21 +3308,16 @@ pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
||||
libc::free(s as *mut _)
|
||||
}
|
||||
|
||||
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]. This is usually preferable over
|
||||
/// [ResultExt::log_err] because warnings go to the logfile and
|
||||
/// errors are displayed directly to the user. Usually problems
|
||||
/// on the FFI layer are coding errors and not errors which need
|
||||
/// to be displayed to the user.
|
||||
/// 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_warn(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E>;
|
||||
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> {
|
||||
@@ -3326,14 +3331,7 @@ 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> {
|
||||
self.map_err(|err| {
|
||||
warn!(context, "{}: {}", message, err);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
fn log_warn(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E> {
|
||||
fn log_err(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E> {
|
||||
self.map_err(|err| {
|
||||
unsafe {
|
||||
wrapper.warning(&format!("{}: {}", message, err));
|
||||
@@ -3343,14 +3341,7 @@ impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,7 +1,8 @@
|
||||
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::constants::*;
|
||||
use deltachat::contact::*;
|
||||
@@ -9,6 +10,7 @@ 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;
|
||||
@@ -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\
|
||||
@@ -395,6 +400,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
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\
|
||||
@@ -511,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_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() {
|
||||
@@ -842,16 +853,42 @@ 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_id.set_archived(context, 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_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.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
@@ -969,7 +1006,10 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
}
|
||||
"setqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
set_config_from_qr(context, arg1);
|
||||
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.");
|
||||
@@ -1010,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::{
|
||||
@@ -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
|
||||
---------
|
||||
|
||||
|
||||
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
|
||||
|
||||
..
|
||||
|
||||
@@ -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):
|
||||
@@ -177,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.
|
||||
"""
|
||||
@@ -382,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.
|
||||
@@ -420,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
|
||||
@@ -528,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
|
||||
@@ -562,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
|
||||
@@ -642,80 +643,6 @@ class IOThreads:
|
||||
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)
|
||||
|
||||
@@ -423,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. """
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import py
|
||||
import pytest
|
||||
import requests
|
||||
@@ -7,6 +8,7 @@ import time
|
||||
from deltachat import Account
|
||||
from deltachat import const
|
||||
from deltachat.capi import lib
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
import tempfile
|
||||
|
||||
|
||||
@@ -43,11 +45,14 @@ 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'],
|
||||
@@ -213,9 +218,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
|
||||
return ac, dict(configdict)
|
||||
|
||||
def get_online_configuring_account(self, mvbox=False, sentbox=False,
|
||||
pre_generated_key=True):
|
||||
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
|
||||
|
||||
@@ -65,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()
|
||||
@@ -81,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):
|
||||
@@ -428,12 +437,6 @@ class TestOfflineChat:
|
||||
|
||||
|
||||
class TestOnlineAccount:
|
||||
@pytest.mark.ignored
|
||||
def test_configure_generate_key(self, acfactory):
|
||||
# A slow test which will generate a new key.
|
||||
ac = acfactory.get_one_online_account(pre_generated_key=False)
|
||||
ac.check_is_configured()
|
||||
|
||||
def get_chat(self, ac1, ac2, both_created=False):
|
||||
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||
chat = ac1.create_chat_by_contact(c2)
|
||||
@@ -442,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)
|
||||
@@ -1338,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]
|
||||
|
||||
@@ -14,6 +14,7 @@ passenv =
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
DCC_PY_LIVECONFIG
|
||||
DCC_NEW_TMP_EMAIL
|
||||
CARGO_TARGET_DIR
|
||||
RUSTC_WRAPPER
|
||||
deps =
|
||||
|
||||
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.
|
||||
|
||||
@@ -75,7 +75,7 @@ impl Aheader {
|
||||
wanted_from: &str,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Option<Self> {
|
||||
if let Ok(Some(value)) = headers.get_header_value(HeaderDef::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) {
|
||||
|
||||
378
src/chat.rs
378
src/chat.rs
@@ -28,7 +28,9 @@ use crate::stock::StockMessage;
|
||||
///
|
||||
/// Some chat IDs are reserved to identify special chat types. This
|
||||
/// type can represent both the special as well as normal chats.
|
||||
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord,
|
||||
)]
|
||||
pub struct ChatId(u32);
|
||||
|
||||
impl ChatId {
|
||||
@@ -135,14 +137,18 @@ impl ChatId {
|
||||
}
|
||||
|
||||
/// Archives or unarchives a chat.
|
||||
pub fn set_archived(self, context: &Context, new_archived: bool) -> Result<(), Error> {
|
||||
pub fn set_visibility(
|
||||
self,
|
||||
context: &Context,
|
||||
visibility: ChatVisibility,
|
||||
) -> Result<(), Error> {
|
||||
ensure!(
|
||||
!self.is_special(),
|
||||
"bad chat_id, can not be special chat: {}",
|
||||
self
|
||||
);
|
||||
|
||||
if new_archived {
|
||||
if visibility == ChatVisibility::Archived {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
@@ -155,7 +161,7 @@ impl ChatId {
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE chats SET archived=? WHERE id=?;",
|
||||
params![new_archived, self],
|
||||
params![visibility, self],
|
||||
)?;
|
||||
|
||||
context.call_cb(Event::MsgsChanged {
|
||||
@@ -166,13 +172,13 @@ impl ChatId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// note that unarchive() is not the same as set_archived(false) -
|
||||
// eg. unarchive() does not send events as done for set_archived(false).
|
||||
// note that unarchive() is not the same as set_visibility(Normal) -
|
||||
// eg. unarchive() does not modify pinned chats and does not send events.
|
||||
pub fn unarchive(self, context: &Context) -> Result<(), Error> {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE chats SET archived=0 WHERE id=?",
|
||||
"UPDATE chats SET archived=0 WHERE id=? and archived=1",
|
||||
params![self],
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -280,10 +286,7 @@ impl ChatId {
|
||||
/// Returns `true`, if message was deleted, `false` otherwise.
|
||||
fn maybe_delete_draft(self, context: &Context) -> bool {
|
||||
match self.get_draft_msg_id(context) {
|
||||
Some(msg_id) => {
|
||||
Message::delete_from_db(context, msg_id);
|
||||
true
|
||||
}
|
||||
Some(msg_id) => msg_id.delete_from_db(context).is_ok(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
@@ -356,6 +359,25 @@ impl ChatId {
|
||||
.unwrap_or_default() as usize
|
||||
}
|
||||
|
||||
pub(crate) fn get_param(self, context: &Context) -> Result<Params, Error> {
|
||||
let res: Option<String> = context
|
||||
.sql
|
||||
.query_get_value_result("SELECT param FROM chats WHERE id=?", params![self])?;
|
||||
Ok(res
|
||||
.map(|s| s.parse().unwrap_or_default())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
// Returns true if chat is a saved messages chat.
|
||||
pub fn is_self_talk(self, context: &Context) -> Result<bool, Error> {
|
||||
Ok(self.get_param(context)?.exists(Param::Selftalk))
|
||||
}
|
||||
|
||||
/// Returns true if chat is a device chat.
|
||||
pub fn is_device_talk(self, context: &Context) -> Result<bool, Error> {
|
||||
Ok(self.get_param(context)?.exists(Param::Devicetalk))
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
///
|
||||
/// Avoid using this, eventually types should be cleaned up enough
|
||||
@@ -414,12 +436,12 @@ impl rusqlite::types::FromSql for ChatId {
|
||||
/// Chat objects are created using eg. `Chat::load_from_db`
|
||||
/// and are not updated on database changes;
|
||||
/// if you want an update, you have to recreate the object.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Chat {
|
||||
pub id: ChatId,
|
||||
pub typ: Chattype,
|
||||
pub name: String,
|
||||
archived: bool,
|
||||
pub visibility: ChatVisibility,
|
||||
pub grpid: String,
|
||||
blocked: Blocked,
|
||||
pub param: Params,
|
||||
@@ -443,7 +465,7 @@ impl Chat {
|
||||
name: row.get::<_, String>(1)?,
|
||||
grpid: row.get::<_, String>(2)?,
|
||||
param: row.get::<_, String>(3)?.parse().unwrap_or_default(),
|
||||
archived: row.get(4)?,
|
||||
visibility: row.get(4)?,
|
||||
blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(),
|
||||
is_sending_locations: row.get(6)?,
|
||||
mute_duration: row.get(7)?,
|
||||
@@ -654,7 +676,7 @@ impl Chat {
|
||||
id: self.id,
|
||||
type_: self.typ as u32,
|
||||
name: self.name.clone(),
|
||||
archived: self.archived,
|
||||
archived: self.visibility == ChatVisibility::Archived,
|
||||
param: self.param.to_string(),
|
||||
gossiped_timestamp: self.get_gossiped_timestamp(context),
|
||||
is_sending_locations: self.is_sending_locations,
|
||||
@@ -666,9 +688,8 @@ impl Chat {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if the chat is archived.
|
||||
pub fn is_archived(&self) -> bool {
|
||||
self.archived
|
||||
pub fn get_visibility(&self) -> ChatVisibility {
|
||||
self.visibility
|
||||
}
|
||||
|
||||
pub fn is_unpromoted(&self) -> bool {
|
||||
@@ -926,6 +947,40 @@ impl Chat {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum ChatVisibility {
|
||||
Normal,
|
||||
Archived,
|
||||
Pinned,
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for ChatVisibility {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
let visibility = match &self {
|
||||
ChatVisibility::Normal => 0,
|
||||
ChatVisibility::Archived => 1,
|
||||
ChatVisibility::Pinned => 2,
|
||||
};
|
||||
let val = rusqlite::types::Value::Integer(visibility);
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::FromSql for ChatVisibility {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||
i64::column_result(value).and_then(|val| {
|
||||
match val {
|
||||
2 => Ok(ChatVisibility::Pinned),
|
||||
1 => Ok(ChatVisibility::Archived),
|
||||
0 => Ok(ChatVisibility::Normal),
|
||||
// fallback to to Normal for unknown values, may happen eg. on imports created by a newer version.
|
||||
_ => Ok(ChatVisibility::Normal),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The current state of a chat.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
@@ -1075,7 +1130,7 @@ pub fn create_by_contact_id(context: &Context, contact_id: u32) -> Result<ChatId
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
pub fn update_saved_messages_icon(context: &Context) -> Result<(), Error> {
|
||||
pub(crate) fn update_saved_messages_icon(context: &Context) -> Result<(), Error> {
|
||||
// if there is no saved-messages chat, there is nothing to update. this is no error.
|
||||
if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_SELF) {
|
||||
let icon = include_bytes!("../assets/icon-saved-messages.png");
|
||||
@@ -1089,7 +1144,7 @@ pub fn update_saved_messages_icon(context: &Context) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_device_icon(context: &Context) -> Result<(), Error> {
|
||||
pub(crate) fn update_device_icon(context: &Context) -> Result<(), Error> {
|
||||
// if there is no device-chat, there is nothing to update. this is no error.
|
||||
if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE) {
|
||||
let icon = include_bytes!("../assets/icon-device.png");
|
||||
@@ -1123,13 +1178,13 @@ fn update_special_chat_name(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_special_chat_names(context: &Context) -> Result<(), Error> {
|
||||
pub(crate) fn update_special_chat_names(context: &Context) -> Result<(), Error> {
|
||||
update_special_chat_name(context, DC_CONTACT_ID_DEVICE, StockMessage::DeviceMessages)?;
|
||||
update_special_chat_name(context, DC_CONTACT_ID_SELF, StockMessage::SavedMessages)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_or_lookup_by_contact_id(
|
||||
pub(crate) fn create_or_lookup_by_contact_id(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
create_blocked: Blocked,
|
||||
@@ -1145,33 +1200,31 @@ pub fn create_or_lookup_by_contact_id(
|
||||
let contact = Contact::load_from_db(context, contact_id)?;
|
||||
let chat_name = contact.get_display_name();
|
||||
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"INSERT INTO chats (type, name, param, blocked, grpid, created_timestamp) VALUES(?, ?, ?, ?, ?, ?)",
|
||||
context
|
||||
.sql
|
||||
.start_stmt("create_or_lookup_by_contact_id transaction");
|
||||
context.sql.with_conn(|conn| {
|
||||
let tx = conn.transaction()?;
|
||||
tx.execute(
|
||||
"INSERT INTO chats (type, name, param, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?)",
|
||||
params![
|
||||
100,
|
||||
Chattype::Single,
|
||||
chat_name,
|
||||
match contact_id {
|
||||
DC_CONTACT_ID_SELF => "K=1".to_string(), // K = Param::Selftalk
|
||||
DC_CONTACT_ID_DEVICE => "D=1".to_string(), // D = Param::Devicetalk
|
||||
_ => "".to_string()
|
||||
_ => "".to_string(),
|
||||
},
|
||||
create_blocked as u8,
|
||||
contact.get_addr(),
|
||||
time(),
|
||||
]
|
||||
)?;
|
||||
|
||||
let row_id = sql::get_rowid(context, &context.sql, "chats", "grpid", contact.get_addr());
|
||||
let chat_id = ChatId::new(row_id);
|
||||
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
params![chat_id, contact_id],
|
||||
)?;
|
||||
)?;
|
||||
tx.execute(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES((SELECT last_insert_rowid()), ?)",
|
||||
params![contact_id])?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
if contact_id == DC_CONTACT_ID_SELF {
|
||||
update_saved_messages_icon(context)?;
|
||||
@@ -1179,10 +1232,10 @@ pub fn create_or_lookup_by_contact_id(
|
||||
update_device_icon(context)?;
|
||||
}
|
||||
|
||||
Ok((chat_id, create_blocked))
|
||||
lookup_by_contact_id(context, contact_id)
|
||||
}
|
||||
|
||||
pub fn lookup_by_contact_id(
|
||||
pub(crate) fn lookup_by_contact_id(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
) -> Result<(ChatId, Blocked), Error> {
|
||||
@@ -1236,7 +1289,7 @@ pub fn prepare_msg<'a>(
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
pub fn msgtype_has_file(msgtype: Viewtype) -> bool {
|
||||
pub(crate) fn msgtype_has_file(msgtype: Viewtype) -> bool {
|
||||
match msgtype {
|
||||
Viewtype::Unknown => false,
|
||||
Viewtype::Text => false,
|
||||
@@ -1413,6 +1466,18 @@ pub fn get_chat_msgs(
|
||||
flags: u32,
|
||||
marker1before: Option<MsgId>,
|
||||
) -> Vec<MsgId> {
|
||||
match hide_device_expired_messages(context) {
|
||||
Err(err) => warn!(context, "Failed to delete expired messages: {}", err),
|
||||
Ok(messages_deleted) => {
|
||||
if messages_deleted {
|
||||
context.call_cb(Event::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let process_row =
|
||||
|row: &rusqlite::Row| Ok((row.get::<_, MsgId>("id")?, row.get::<_, i64>("timestamp")?));
|
||||
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
||||
@@ -1547,6 +1612,47 @@ pub fn marknoticed_all_chats(context: &Context) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hides messages which are expired according to "delete_device_after" setting.
|
||||
///
|
||||
/// Returns true if any message is hidden, so event can be emitted. If nothing
|
||||
/// has been hidden, returns false.
|
||||
pub fn hide_device_expired_messages(context: &Context) -> Result<bool, Error> {
|
||||
if let Some(delete_device_after) = context.get_config_delete_device_after() {
|
||||
let threshold_timestamp = time() - delete_device_after;
|
||||
|
||||
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
|
||||
// Hide expired messages
|
||||
//
|
||||
// Only update the rows that have to be updated, to avoid emitting
|
||||
// unnecessary "chat modified" events.
|
||||
let rows_modified = context.sql.execute(
|
||||
"UPDATE msgs \
|
||||
SET txt = 'DELETED', hidden = 1 \
|
||||
WHERE timestamp < ? \
|
||||
AND chat_id > ? \
|
||||
AND chat_id != ? \
|
||||
AND chat_id != ? \
|
||||
AND NOT hidden",
|
||||
params![
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
device_chat_id
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(rows_modified > 0)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_chat_media(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
@@ -1713,9 +1819,11 @@ pub fn create_group_chat(
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/* you MUST NOT modify this or the following strings */
|
||||
// Context functions to work with chats
|
||||
pub fn add_to_chat_contacts_table(context: &Context, chat_id: ChatId, contact_id: u32) -> bool {
|
||||
pub(crate) fn add_to_chat_contacts_table(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
) -> bool {
|
||||
// add a contact to a chat; the function does not check the type or if any of the record exist or are already
|
||||
// added to the chat!
|
||||
sql::execute(
|
||||
@@ -1727,6 +1835,22 @@ pub fn add_to_chat_contacts_table(context: &Context, chat_id: ChatId, contact_id
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_from_chat_contacts_table(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: u32,
|
||||
) -> bool {
|
||||
// remove a contact from the chats_contact table unconditionally
|
||||
// the function does not check the type or if the record exist
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
|
||||
params![chat_id, contact_id as i32],
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Adds a contact to the chat.
|
||||
pub fn add_contact_to_chat(context: &Context, chat_id: ChatId, contact_id: u32) -> bool {
|
||||
match add_contact_to_chat_ex(context, chat_id, contact_id, false) {
|
||||
@@ -1847,7 +1971,7 @@ fn real_group_exists(context: &Context, chat_id: ChatId) -> bool {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn reset_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<(), Error> {
|
||||
pub(crate) fn reset_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<(), Error> {
|
||||
set_gossiped_timestamp(context, chat_id, 0)
|
||||
}
|
||||
|
||||
@@ -1864,7 +1988,7 @@ pub fn get_gossiped_timestamp(context: &Context, chat_id: ChatId) -> i64 {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn set_gossiped_timestamp(
|
||||
pub(crate) fn set_gossiped_timestamp(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
timestamp: i64,
|
||||
@@ -1884,7 +2008,7 @@ pub fn set_gossiped_timestamp(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool, Error> {
|
||||
pub(crate) fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool, Error> {
|
||||
// versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others.
|
||||
// to avoid sending out previously set selfavatars unexpectedly we added this additional check.
|
||||
// it can be removed after some time.
|
||||
@@ -1919,7 +2043,7 @@ pub fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<boo
|
||||
Ok(needs_attach)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum MuteDuration {
|
||||
NotMuted,
|
||||
Forever,
|
||||
@@ -2008,7 +2132,7 @@ pub fn remove_contact_from_chat(
|
||||
"Cannot remove contact from chat; self not in group.".into()
|
||||
)
|
||||
);
|
||||
} else {
|
||||
} else if remove_from_chat_contacts_table(context, chat_id, contact_id) {
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id) {
|
||||
if chat.is_promoted() {
|
||||
@@ -2038,17 +2162,9 @@ pub fn remove_contact_from_chat(
|
||||
});
|
||||
}
|
||||
}
|
||||
if sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
|
||||
params![chat_id, contact_id as i32],
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
success = true;
|
||||
}
|
||||
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2073,7 +2189,10 @@ fn set_group_explicitly_left(context: &Context, grpid: impl AsRef<str>) -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_group_explicitly_left(context: &Context, grpid: impl AsRef<str>) -> Result<bool, Error> {
|
||||
pub(crate) fn is_group_explicitly_left(
|
||||
context: &Context,
|
||||
grpid: impl AsRef<str>,
|
||||
) -> Result<bool, Error> {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
@@ -2295,7 +2414,7 @@ pub fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_chat_contact_cnt(context: &Context, chat_id: ChatId) -> usize {
|
||||
pub(crate) fn get_chat_contact_cnt(context: &Context, chat_id: ChatId) -> usize {
|
||||
context
|
||||
.sql
|
||||
.query_get_value::<_, isize>(
|
||||
@@ -2306,7 +2425,7 @@ pub fn get_chat_contact_cnt(context: &Context, chat_id: ChatId) -> usize {
|
||||
.unwrap_or_default() as usize
|
||||
}
|
||||
|
||||
pub fn get_chat_cnt(context: &Context) -> usize {
|
||||
pub(crate) fn get_chat_cnt(context: &Context) -> usize {
|
||||
if context.sql.is_open() {
|
||||
/* no database, no chats - this is no error (needed eg. for information) */
|
||||
context
|
||||
@@ -2421,7 +2540,7 @@ pub fn was_device_msg_ever_added(context: &Context, label: &str) -> Result<bool,
|
||||
// no wrong information are shown in the device chat
|
||||
// - deletion in `devmsglabels` makes sure,
|
||||
// deleted messages are resetted and useful messages can be added again
|
||||
pub fn delete_and_reset_all_device_msgs(context: &Context) -> Result<(), Error> {
|
||||
pub(crate) fn delete_and_reset_all_device_msgs(context: &Context) -> Result<(), Error> {
|
||||
context.sql.execute(
|
||||
"DELETE FROM msgs WHERE from_id=?;",
|
||||
params![DC_CONTACT_ID_DEVICE],
|
||||
@@ -2435,7 +2554,7 @@ pub fn delete_and_reset_all_device_msgs(context: &Context) -> Result<(), Error>
|
||||
/// Adds an informational message to chat.
|
||||
///
|
||||
/// For example, it can be a message showing that a member was added to a group.
|
||||
pub fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef<str>) {
|
||||
pub(crate) fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef<str>) {
|
||||
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
|
||||
|
||||
if context.sql.execute(
|
||||
@@ -2558,7 +2677,7 @@ mod tests {
|
||||
let chat = Chat::load_from_db(&t.ctx, chat_id).unwrap();
|
||||
assert_eq!(chat.id, chat_id);
|
||||
assert!(chat.is_self_talk());
|
||||
assert!(!chat.archived);
|
||||
assert!(chat.visibility == ChatVisibility::Normal);
|
||||
assert!(!chat.is_device_talk());
|
||||
assert!(chat.can_send());
|
||||
assert_eq!(chat.name, t.ctx.stock_str(StockMessage::SavedMessages));
|
||||
@@ -2572,7 +2691,7 @@ mod tests {
|
||||
assert_eq!(DC_CHAT_ID_DEADDROP, 1);
|
||||
assert!(chat.id.is_deaddrop());
|
||||
assert!(!chat.is_self_talk());
|
||||
assert!(!chat.archived);
|
||||
assert!(chat.visibility == ChatVisibility::Normal);
|
||||
assert!(!chat.is_device_talk());
|
||||
assert!(!chat.can_send());
|
||||
assert_eq!(chat.name, t.ctx.stock_str(StockMessage::DeadDrop));
|
||||
@@ -2778,35 +2897,134 @@ mod tests {
|
||||
assert_eq!(DC_GCL_NO_SPECIALS, 0x02);
|
||||
|
||||
// archive first chat
|
||||
assert!(chat_id1.set_archived(&t.ctx, true).is_ok());
|
||||
assert!(Chat::load_from_db(&t.ctx, chat_id1).unwrap().is_archived());
|
||||
assert!(!Chat::load_from_db(&t.ctx, chat_id2).unwrap().is_archived());
|
||||
assert!(chat_id1
|
||||
.set_visibility(&t.ctx, ChatVisibility::Archived)
|
||||
.is_ok());
|
||||
assert!(
|
||||
Chat::load_from_db(&t.ctx, chat_id1)
|
||||
.unwrap()
|
||||
.get_visibility()
|
||||
== ChatVisibility::Archived
|
||||
);
|
||||
assert!(
|
||||
Chat::load_from_db(&t.ctx, chat_id2)
|
||||
.unwrap()
|
||||
.get_visibility()
|
||||
== ChatVisibility::Normal
|
||||
);
|
||||
assert_eq!(get_chat_cnt(&t.ctx), 2);
|
||||
assert_eq!(chatlist_len(&t.ctx, 0), 2); // including DC_CHAT_ID_ARCHIVED_LINK now
|
||||
assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS), 1);
|
||||
assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 1);
|
||||
|
||||
// archive second chat
|
||||
assert!(chat_id2.set_archived(&t.ctx, true).is_ok());
|
||||
assert!(Chat::load_from_db(&t.ctx, chat_id1).unwrap().is_archived());
|
||||
assert!(Chat::load_from_db(&t.ctx, chat_id2).unwrap().is_archived());
|
||||
assert!(chat_id2
|
||||
.set_visibility(&t.ctx, ChatVisibility::Archived)
|
||||
.is_ok());
|
||||
assert!(
|
||||
Chat::load_from_db(&t.ctx, chat_id1)
|
||||
.unwrap()
|
||||
.get_visibility()
|
||||
== ChatVisibility::Archived
|
||||
);
|
||||
assert!(
|
||||
Chat::load_from_db(&t.ctx, chat_id2)
|
||||
.unwrap()
|
||||
.get_visibility()
|
||||
== ChatVisibility::Archived
|
||||
);
|
||||
assert_eq!(get_chat_cnt(&t.ctx), 2);
|
||||
assert_eq!(chatlist_len(&t.ctx, 0), 1); // only DC_CHAT_ID_ARCHIVED_LINK now
|
||||
assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS), 0);
|
||||
assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 2);
|
||||
|
||||
// archive already archived first chat, unarchive second chat two times
|
||||
assert!(chat_id1.set_archived(&t.ctx, true).is_ok());
|
||||
assert!(chat_id2.set_archived(&t.ctx, false).is_ok());
|
||||
assert!(chat_id2.set_archived(&t.ctx, false).is_ok());
|
||||
assert!(Chat::load_from_db(&t.ctx, chat_id1).unwrap().is_archived());
|
||||
assert!(!Chat::load_from_db(&t.ctx, chat_id2).unwrap().is_archived());
|
||||
assert!(chat_id1
|
||||
.set_visibility(&t.ctx, ChatVisibility::Archived)
|
||||
.is_ok());
|
||||
assert!(chat_id2
|
||||
.set_visibility(&t.ctx, ChatVisibility::Normal)
|
||||
.is_ok());
|
||||
assert!(chat_id2
|
||||
.set_visibility(&t.ctx, ChatVisibility::Normal)
|
||||
.is_ok());
|
||||
assert!(
|
||||
Chat::load_from_db(&t.ctx, chat_id1)
|
||||
.unwrap()
|
||||
.get_visibility()
|
||||
== ChatVisibility::Archived
|
||||
);
|
||||
assert!(
|
||||
Chat::load_from_db(&t.ctx, chat_id2)
|
||||
.unwrap()
|
||||
.get_visibility()
|
||||
== ChatVisibility::Normal
|
||||
);
|
||||
assert_eq!(get_chat_cnt(&t.ctx), 2);
|
||||
assert_eq!(chatlist_len(&t.ctx, 0), 2);
|
||||
assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS), 1);
|
||||
assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 1);
|
||||
}
|
||||
|
||||
fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec<ChatId> {
|
||||
let chatlist = Chatlist::try_load(ctx, listflags, None, None).unwrap();
|
||||
let mut result = Vec::new();
|
||||
for chatlist_index in 0..chatlist.len() {
|
||||
result.push(chatlist.get_chat_id(chatlist_index))
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pinned() {
|
||||
let t = dummy_context();
|
||||
|
||||
// create 3 chats, wait 1 second in between to get a reliable order (we order by time)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("foo".to_string());
|
||||
let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap();
|
||||
let chat_id1 = message::Message::load_from_db(&t.ctx, msg_id)
|
||||
.unwrap()
|
||||
.chat_id;
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
let chat_id2 = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap();
|
||||
|
||||
let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS);
|
||||
assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]);
|
||||
|
||||
// pin
|
||||
assert!(chat_id1
|
||||
.set_visibility(&t.ctx, ChatVisibility::Pinned)
|
||||
.is_ok());
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&t.ctx, chat_id1)
|
||||
.unwrap()
|
||||
.get_visibility(),
|
||||
ChatVisibility::Pinned
|
||||
);
|
||||
|
||||
// check if chat order changed
|
||||
let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS);
|
||||
assert_eq!(chatlist, vec![chat_id1, chat_id3, chat_id2]);
|
||||
|
||||
// unpin
|
||||
assert!(chat_id1
|
||||
.set_visibility(&t.ctx, ChatVisibility::Normal)
|
||||
.is_ok());
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&t.ctx, chat_id1)
|
||||
.unwrap()
|
||||
.get_visibility(),
|
||||
ChatVisibility::Normal
|
||||
);
|
||||
|
||||
// check if chat order changed back
|
||||
let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS);
|
||||
assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_chat_name() {
|
||||
let t = dummy_context();
|
||||
|
||||
109
src/chatlist.rs
109
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
|
||||
@@ -388,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_id1.set_archived(&t.ctx, 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();
|
||||
@@ -415,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,13 @@ 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;
|
||||
@@ -62,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,
|
||||
@@ -125,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<()> {
|
||||
@@ -168,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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,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);
|
||||
@@ -369,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 {
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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.
|
||||
@@ -478,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,
|
||||
@@ -502,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,
|
||||
@@ -153,7 +153,7 @@ 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,
|
||||
@@ -181,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));
|
||||
}
|
||||
@@ -192,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!(
|
||||
@@ -212,7 +219,7 @@ pub fn dc_receive_imf(
|
||||
|
||||
cleanup(context, &create_event_to_send, created_db_entries);
|
||||
|
||||
mime_parser.handle_reports(context, from_id, sent_timestamp, &server_folder, server_uid);
|
||||
mime_parser.handle_reports(context, from_id, sent_timestamp);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -274,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)>,
|
||||
@@ -333,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) {
|
||||
@@ -358,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;
|
||||
@@ -389,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,
|
||||
@@ -472,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(
|
||||
@@ -541,7 +570,7 @@ fn add_parts(
|
||||
*chat_id,
|
||||
from_id,
|
||||
*sent_timestamp,
|
||||
0 == flags & DC_IMAP_SEEN,
|
||||
!seen,
|
||||
&mut sort_timestamp,
|
||||
sent_timestamp,
|
||||
&mut rcvd_timestamp,
|
||||
@@ -579,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;
|
||||
@@ -860,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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -985,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 {
|
||||
@@ -1004,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;
|
||||
@@ -1022,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 {
|
||||
@@ -1472,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;
|
||||
@@ -1484,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(
|
||||
@@ -1493,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()
|
||||
}
|
||||
@@ -1520,7 +1555,7 @@ fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) -
|
||||
}
|
||||
|
||||
pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
||||
if let Ok(ids) = mailparse::addrparse(mid_list) {
|
||||
if let Ok(ids) = mailparse::msgidparse(mid_list) {
|
||||
for id in ids.iter() {
|
||||
if is_msgrmsg_rfc724_mid(context, id) {
|
||||
return true;
|
||||
@@ -1530,21 +1565,14 @@ pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -
|
||||
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()
|
||||
}
|
||||
@@ -1629,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() {
|
||||
@@ -1688,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
@@ -538,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[...]",
|
||||
);
|
||||
}
|
||||
@@ -701,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!(
|
||||
@@ -719,11 +706,7 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
src/e2ee.rs
15
src/e2ee.rs
@@ -8,6 +8,7 @@ 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::*;
|
||||
@@ -125,7 +126,7 @@ pub fn try_decrypt(
|
||||
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
|
||||
let from = mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::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)
|
||||
@@ -211,11 +212,11 @@ fn load_or_generate_self_public_key(
|
||||
}
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
info!(
|
||||
context,
|
||||
"Generating keypair with {} bits, e={} ...", 2048, 65537,
|
||||
);
|
||||
let keypair = pgp::create_keypair(EmailAddress::new(self_addr.as_ref())?)?;
|
||||
|
||||
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,
|
||||
@@ -416,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";
|
||||
@@ -428,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;
|
||||
|
||||
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,5 +1,5 @@
|
||||
use crate::strum::AsStaticRef;
|
||||
use mailparse::{MailHeader, MailHeaderMap, MailParseError};
|
||||
use mailparse::{MailHeader, MailHeaderMap};
|
||||
|
||||
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
@@ -26,7 +26,6 @@ pub enum HeaderDef {
|
||||
ChatGroupName,
|
||||
ChatGroupNameChanged,
|
||||
ChatVerified,
|
||||
ChatGroupImage, // deprecated
|
||||
ChatGroupAvatar,
|
||||
ChatUserAvatar,
|
||||
ChatVoiceMessage,
|
||||
@@ -53,11 +52,11 @@ impl HeaderDef {
|
||||
}
|
||||
|
||||
pub trait HeaderDefMap {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError>;
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
|
||||
}
|
||||
|
||||
impl HeaderDefMap for [MailHeader<'_>] {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError> {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
|
||||
self.get_first_value(headerdef.get_headername())
|
||||
}
|
||||
}
|
||||
@@ -80,18 +79,13 @@ mod tests {
|
||||
let (headers, _) =
|
||||
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
|
||||
assert_eq!(
|
||||
headers
|
||||
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
||||
.unwrap(),
|
||||
headers.get_header_value(HeaderDef::AutocryptSetupMessage),
|
||||
Some("v99".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::From_).unwrap(),
|
||||
headers.get_header_value(HeaderDef::From_),
|
||||
Some("Bob".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get_header_value(HeaderDef::Autocrypt).unwrap(),
|
||||
None
|
||||
);
|
||||
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 })
|
||||
|
||||
322
src/imap/mod.rs
322
src/imap/mod.rs
@@ -22,7 +22,6 @@ use crate::dc_receive_imf::{
|
||||
};
|
||||
use crate::events::Event;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::imap_client::*;
|
||||
use crate::job::{job_add, Action};
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::message::{self, update_server_uid};
|
||||
@@ -30,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>;
|
||||
|
||||
@@ -182,7 +184,10 @@ struct ImapConfig {
|
||||
pub selected_mailbox: Option<Mailbox>,
|
||||
pub selected_folder_needs_expunge: bool,
|
||||
pub can_idle: 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 {
|
||||
@@ -199,7 +204,7 @@ impl Default for ImapConfig {
|
||||
selected_mailbox: None,
|
||||
selected_folder_needs_expunge: false,
|
||||
can_idle: false,
|
||||
imap_delimiter: '.',
|
||||
can_move: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,6 +357,7 @@ impl Imap {
|
||||
cfg.imap_port = 0;
|
||||
|
||||
cfg.can_idle = false;
|
||||
cfg.can_move = false;
|
||||
}
|
||||
|
||||
/// Connects to imap account using already-configured parameters.
|
||||
@@ -412,6 +418,7 @@ impl Imap {
|
||||
true
|
||||
} else {
|
||||
let can_idle = caps.has_str("IDLE");
|
||||
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)
|
||||
@@ -421,6 +428,7 @@ impl Imap {
|
||||
});
|
||||
|
||||
self.config.write().await.can_idle = can_idle;
|
||||
self.config.write().await.can_move = can_move;
|
||||
*self.connected.lock().await = true;
|
||||
emit_event!(
|
||||
context,
|
||||
@@ -588,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,
|
||||
@@ -745,32 +753,15 @@ impl Imap {
|
||||
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,
|
||||
@@ -781,18 +772,28 @@ impl Imap {
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Message #{} does not exist in folder \"{}\".",
|
||||
server_uid,
|
||||
folder.as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn can_move(&self) -> bool {
|
||||
task::block_on(async move { self.config.read().await.can_move })
|
||||
}
|
||||
|
||||
pub fn mv(
|
||||
&self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
uid: u32,
|
||||
dest_folder: &str,
|
||||
dest_uid: &mut u32,
|
||||
) -> ImapActionResult {
|
||||
task::block_on(async move {
|
||||
if folder == dest_folder {
|
||||
@@ -809,72 +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);
|
||||
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 {} copied to {} (delete FAILED)",
|
||||
"IMAP Message {} moved to {}",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
);
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
self.config.write().await.selected_folder_needs_expunge = true;
|
||||
emit_event!(
|
||||
return ImapActionResult::Success;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
Event::ImapMessageMoved(format!(
|
||||
"IMAP Message {} copied to {} (delete successfull)",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
|
||||
folder,
|
||||
uid,
|
||||
dest_folder,
|
||||
err
|
||||
);
|
||||
ImapActionResult::Success
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -979,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
|
||||
@@ -1010,7 +1014,7 @@ impl Imap {
|
||||
display_imap_id,
|
||||
message_id,
|
||||
);
|
||||
return ImapActionResult::Failed;
|
||||
return ImapActionResult::AlreadyDone;
|
||||
};
|
||||
|
||||
let remote_message_id = get_fetch_headers(fetch)
|
||||
@@ -1025,7 +1029,7 @@ impl Imap {
|
||||
remote_message_id,
|
||||
message_id,
|
||||
);
|
||||
*uid = 0;
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1033,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,
|
||||
@@ -1063,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);
|
||||
@@ -1093,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
|
||||
@@ -1157,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(())
|
||||
@@ -1194,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;
|
||||
}
|
||||
@@ -1220,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
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1273,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 {
|
||||
@@ -1295,17 +1358,6 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_message_id(value: &str) -> crate::error::Result<String> {
|
||||
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,
|
||||
@@ -1316,30 +1368,27 @@ fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>>
|
||||
}
|
||||
|
||||
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
|
||||
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId)? {
|
||||
Ok(parse_message_id(&message_id)?)
|
||||
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],
|
||||
) -> Result<bool> {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo)? {
|
||||
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 Ok(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::References)? {
|
||||
if let Some(value) = headers.get_header_value(HeaderDef::References) {
|
||||
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
|
||||
return Ok(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
false
|
||||
}
|
||||
|
||||
fn prefetch_should_download(
|
||||
@@ -1347,16 +1396,16 @@ fn prefetch_should_download(
|
||||
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)?;
|
||||
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)?
|
||||
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
||||
.is_some();
|
||||
|
||||
let from_field = headers
|
||||
.get_header_value(HeaderDef::From_)?
|
||||
.get_header_value(HeaderDef::From_)
|
||||
.unwrap_or_default();
|
||||
|
||||
let (_contact_id, blocked_contact, origin) = from_field_to_contact_id(context, &from_field)?;
|
||||
@@ -1373,20 +1422,3 @@ fn prefetch_should_download(
|
||||
let show = show && !blocked_contact;
|
||||
Ok(show)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_message_id() {
|
||||
assert_eq!(
|
||||
parse_message_id("Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
|
||||
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_message_id("<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
|
||||
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +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_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))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub async fn capabilities(&mut self) -> ImapResult<Capabilities> {
|
||||
let res = match self {
|
||||
@@ -227,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>,
|
||||
@@ -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::*;
|
||||
@@ -414,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);
|
||||
@@ -757,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"));
|
||||
}
|
||||
|
||||
|
||||
279
src/job.rs
279
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.
|
||||
@@ -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 */
|
||||
@@ -799,7 +831,11 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
|
||||
.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)
|
||||
@@ -875,6 +911,41 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn perform_inbox_jobs(context: &Context) {
|
||||
info!(context, "dc_perform_inbox_jobs starting.",);
|
||||
|
||||
@@ -882,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.",);
|
||||
}
|
||||
@@ -919,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
|
||||
@@ -1055,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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
189
src/message.rs
189
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 {
|
||||
@@ -77,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
|
||||
@@ -145,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,
|
||||
|
||||
@@ -168,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,
|
||||
@@ -286,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());
|
||||
@@ -424,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> {
|
||||
@@ -607,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,
|
||||
@@ -783,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);
|
||||
@@ -920,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,
|
||||
@@ -946,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,
|
||||
@@ -1115,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.
|
||||
@@ -1314,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),
|
||||
) {
|
||||
@@ -1358,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(_) => {}
|
||||
|
||||
@@ -68,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)?;
|
||||
|
||||
@@ -156,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)
|
||||
@@ -368,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()));
|
||||
@@ -380,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()));
|
||||
}
|
||||
@@ -638,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()));
|
||||
@@ -681,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();
|
||||
@@ -702,7 +704,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
"0".to_string(),
|
||||
));
|
||||
}
|
||||
add_compatibility_header = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -770,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 {
|
||||
@@ -880,7 +870,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
|
||||
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))
|
||||
@@ -1075,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),
|
||||
|
||||
@@ -5,8 +5,8 @@ use lettre_email::mime::{self, Mime};
|
||||
use mailparse::{DispositionType, MailAddr, MailHeaderMap};
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::bail;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
@@ -16,7 +16,6 @@ use crate::e2ee;
|
||||
use crate::error::Result;
|
||||
use crate::events::Event;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::job::{job_add, Action};
|
||||
use crate::location;
|
||||
use crate::message;
|
||||
use crate::param::*;
|
||||
@@ -24,7 +23,6 @@ use crate::peerstate::Peerstate;
|
||||
use crate::securejoin::handle_degrade_event;
|
||||
use crate::simplify::*;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::{bail, ensure};
|
||||
|
||||
/// A parsed MIME message.
|
||||
///
|
||||
@@ -46,28 +44,17 @@ pub struct MimeMessage {
|
||||
pub is_system_message: SystemMessage,
|
||||
pub location_kml: Option<location::Kml>,
|
||||
pub message_kml: Option<location::Kml>,
|
||||
pub user_avatar: AvatarAction,
|
||||
pub group_avatar: AvatarAction,
|
||||
pub(crate) user_avatar: Option<AvatarAction>,
|
||||
pub(crate) group_avatar: Option<AvatarAction>,
|
||||
pub(crate) reports: Vec<Report>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AvatarAction {
|
||||
None,
|
||||
pub(crate) enum AvatarAction {
|
||||
Delete,
|
||||
Change(String),
|
||||
}
|
||||
|
||||
impl AvatarAction {
|
||||
pub fn is_change(&self) -> bool {
|
||||
match self {
|
||||
AvatarAction::None => false,
|
||||
AvatarAction::Delete => false,
|
||||
AvatarAction::Change(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
|
||||
#[repr(i32)]
|
||||
pub enum SystemMessage {
|
||||
@@ -96,7 +83,7 @@ impl MimeMessage {
|
||||
|
||||
let message_time = mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::Date)?
|
||||
.get_header_value(HeaderDef::Date)
|
||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -122,8 +109,7 @@ impl MimeMessage {
|
||||
|
||||
// Handle any gossip headers if the mail was encrypted. See section
|
||||
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
|
||||
let gossip_headers =
|
||||
decrypted_mail.headers.get_all_values("Autocrypt-Gossip")?;
|
||||
let gossip_headers = decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossipped_addr =
|
||||
update_gossip_peerstates(context, message_time, &mail, gossip_headers)?;
|
||||
|
||||
@@ -163,8 +149,8 @@ impl MimeMessage {
|
||||
is_system_message: SystemMessage::Unknown,
|
||||
location_kml: None,
|
||||
message_kml: None,
|
||||
user_avatar: AvatarAction::None,
|
||||
group_avatar: AvatarAction::None,
|
||||
user_avatar: None,
|
||||
group_avatar: None,
|
||||
};
|
||||
parser.parse_mime_recursive(context, &mail)?;
|
||||
parser.parse_headers(context)?;
|
||||
@@ -202,10 +188,6 @@ impl MimeMessage {
|
||||
fn parse_avatar_headers(&mut self) {
|
||||
if let Some(header_value) = self.get(HeaderDef::ChatGroupAvatar).cloned() {
|
||||
self.group_avatar = self.avatar_action_from_header(header_value);
|
||||
} else if let Some(header_value) = self.get(HeaderDef::ChatGroupImage).cloned() {
|
||||
// parse the old group-image headers for versions <=0.973 resp. <=beta.15 (december 2019)
|
||||
// grep for #DeprecatedAvatar to get the place where a compatibility header is generated.
|
||||
self.group_avatar = self.avatar_action_from_header(header_value);
|
||||
}
|
||||
|
||||
if let Some(header_value) = self.get(HeaderDef::ChatUserAvatar).cloned() {
|
||||
@@ -360,9 +342,9 @@ impl MimeMessage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn avatar_action_from_header(&mut self, header_value: String) -> AvatarAction {
|
||||
fn avatar_action_from_header(&mut self, header_value: String) -> Option<AvatarAction> {
|
||||
if header_value == "0" {
|
||||
return AvatarAction::Delete;
|
||||
Some(AvatarAction::Delete)
|
||||
} else {
|
||||
let mut i = 0;
|
||||
while i != self.parts.len() {
|
||||
@@ -370,7 +352,7 @@ impl MimeMessage {
|
||||
if let Some(part_filename) = &part.org_filename {
|
||||
if part_filename == &header_value {
|
||||
if let Some(blob) = part.param.get(Param::File) {
|
||||
let res = AvatarAction::Change(blob.to_string());
|
||||
let res = Some(AvatarAction::Change(blob.to_string()));
|
||||
self.parts.remove(i);
|
||||
return res;
|
||||
}
|
||||
@@ -379,8 +361,8 @@ impl MimeMessage {
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
AvatarAction::None
|
||||
}
|
||||
|
||||
pub fn was_encrypted(&self) -> bool {
|
||||
@@ -560,6 +542,16 @@ impl MimeMessage {
|
||||
if let Some(report) = self.process_report(context, mail)? {
|
||||
self.reports.push(report);
|
||||
}
|
||||
|
||||
// Add MDN part so we can track it, avoid
|
||||
// downloading the message again and
|
||||
// delete if automatic message deletion is
|
||||
// enabled.
|
||||
let mut part = Part::default();
|
||||
part.typ = Viewtype::Unknown;
|
||||
self.parts.push(part);
|
||||
|
||||
any_part_added = true;
|
||||
} else {
|
||||
/* eg. `report-type=delivery-status`;
|
||||
maybe we should show them as a little error icon */
|
||||
@@ -593,60 +585,64 @@ impl MimeMessage {
|
||||
let (mime_type, msg_type) = get_mime_type(mail)?;
|
||||
let raw_mime = mail.ctype.mimetype.to_lowercase();
|
||||
|
||||
let filename = get_attachment_filename(mail);
|
||||
let filename = get_attachment_filename(mail)?;
|
||||
|
||||
let old_part_count = self.parts.len();
|
||||
|
||||
if let Ok(filename) = filename {
|
||||
self.do_add_single_file_part(
|
||||
context,
|
||||
msg_type,
|
||||
mime_type,
|
||||
&raw_mime,
|
||||
&mail.get_body_raw()?,
|
||||
&filename,
|
||||
);
|
||||
} else {
|
||||
match mime_type.type_() {
|
||||
mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
|
||||
bail!("missing attachment");
|
||||
}
|
||||
mime::TEXT | mime::HTML => {
|
||||
let decoded_data = match mail.get_body() {
|
||||
Ok(decoded_data) => decoded_data,
|
||||
Err(err) => {
|
||||
warn!(context, "Invalid body parsed {:?}", err);
|
||||
// Note that it's not always an error - might be no data
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
let (simplified_txt, is_forwarded) = if decoded_data.is_empty() {
|
||||
("".into(), false)
|
||||
} else {
|
||||
let is_html = mime_type == mime::TEXT_HTML;
|
||||
let out = if is_html {
|
||||
dehtml(&decoded_data)
|
||||
} else {
|
||||
decoded_data.clone()
|
||||
match filename {
|
||||
Some(filename) => {
|
||||
self.do_add_single_file_part(
|
||||
context,
|
||||
msg_type,
|
||||
mime_type,
|
||||
&raw_mime,
|
||||
&mail.get_body_raw()?,
|
||||
&filename,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
match mime_type.type_() {
|
||||
mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
|
||||
warn!(context, "Missing attachment");
|
||||
return Ok(false);
|
||||
}
|
||||
mime::TEXT | mime::HTML => {
|
||||
let decoded_data = match mail.get_body() {
|
||||
Ok(decoded_data) => decoded_data,
|
||||
Err(err) => {
|
||||
warn!(context, "Invalid body parsed {:?}", err);
|
||||
// Note that it's not always an error - might be no data
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
simplify(out, self.has_chat_version())
|
||||
};
|
||||
|
||||
if !simplified_txt.is_empty() {
|
||||
let mut part = Part::default();
|
||||
part.typ = Viewtype::Text;
|
||||
part.mimetype = Some(mime_type);
|
||||
part.msg = simplified_txt;
|
||||
part.msg_raw = Some(decoded_data);
|
||||
self.do_add_single_part(part);
|
||||
}
|
||||
let (simplified_txt, is_forwarded) = if decoded_data.is_empty() {
|
||||
("".into(), false)
|
||||
} else {
|
||||
let is_html = mime_type == mime::TEXT_HTML;
|
||||
let out = if is_html {
|
||||
dehtml(&decoded_data)
|
||||
} else {
|
||||
decoded_data.clone()
|
||||
};
|
||||
simplify(out, self.has_chat_version())
|
||||
};
|
||||
|
||||
if is_forwarded {
|
||||
self.is_forwarded = true;
|
||||
if !simplified_txt.is_empty() {
|
||||
let mut part = Part::default();
|
||||
part.typ = Viewtype::Text;
|
||||
part.mimetype = Some(mime_type);
|
||||
part.msg = simplified_txt;
|
||||
part.msg_raw = Some(decoded_data);
|
||||
self.do_add_single_part(part);
|
||||
}
|
||||
|
||||
if is_forwarded {
|
||||
self.is_forwarded = true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,21 +748,18 @@ impl MimeMessage {
|
||||
|
||||
pub fn get_rfc724_mid(&self) -> Option<String> {
|
||||
self.get(HeaderDef::MessageId)
|
||||
.and_then(|msgid| parse_message_id(msgid))
|
||||
.and_then(|msgid| parse_message_id(msgid).ok())
|
||||
}
|
||||
|
||||
fn merge_headers(headers: &mut HashMap<String, String>, fields: &[mailparse::MailHeader<'_>]) {
|
||||
for field in fields {
|
||||
if let Ok(key) = field.get_key() {
|
||||
// lowercasing all headers is technically not correct, but makes things work better
|
||||
let key = key.to_lowercase();
|
||||
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
|
||||
// lowercasing all headers is technically not correct, but makes things work better
|
||||
let key = field.get_key().to_lowercase();
|
||||
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
|
||||
is_known(&key) || key.starts_with("chat-")
|
||||
{
|
||||
if let Ok(value) = field.get_value() {
|
||||
headers.insert(key, value);
|
||||
}
|
||||
}
|
||||
{
|
||||
let value = field.get_value();
|
||||
headers.insert(key.to_string(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -781,23 +774,17 @@ impl MimeMessage {
|
||||
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
|
||||
|
||||
// must be present
|
||||
if let Some(_disposition) = report_fields
|
||||
.get_header_value(HeaderDef::Disposition)
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
|
||||
if let Some(original_message_id) = report_fields
|
||||
.get_header_value(HeaderDef::OriginalMessageId)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|v| parse_message_id(&v))
|
||||
.and_then(|v| parse_message_id(&v).ok())
|
||||
{
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map_or_else(Vec::new, |v| {
|
||||
v.split(' ').filter_map(parse_message_id).collect()
|
||||
v.split(' ')
|
||||
.filter_map(|s| parse_message_id(s).ok())
|
||||
.collect()
|
||||
});
|
||||
|
||||
return Ok(Some(Report {
|
||||
@@ -809,26 +796,18 @@ impl MimeMessage {
|
||||
warn!(
|
||||
context,
|
||||
"ignoring unknown disposition-notification, Message-Id: {:?}",
|
||||
report_fields.get_header_value(HeaderDef::MessageId).ok()
|
||||
report_fields.get_header_value(HeaderDef::MessageId)
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Handle reports (only MDNs for now)
|
||||
pub fn handle_reports(
|
||||
&self,
|
||||
context: &Context,
|
||||
from_id: u32,
|
||||
sent_timestamp: i64,
|
||||
server_folder: impl AsRef<str>,
|
||||
server_uid: u32,
|
||||
) {
|
||||
pub fn handle_reports(&self, context: &Context, from_id: u32, sent_timestamp: i64) {
|
||||
if self.reports.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut mdn_recognized = false;
|
||||
for report in &self.reports {
|
||||
for original_message_id in
|
||||
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
|
||||
@@ -837,20 +816,9 @@ impl MimeMessage {
|
||||
message::mdn_from_ext(context, from_id, original_message_id, sent_timestamp)
|
||||
{
|
||||
context.call_cb(Event::MsgRead { chat_id, msg_id });
|
||||
mdn_recognized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.has_chat_version() || mdn_recognized {
|
||||
let mut param = Params::new();
|
||||
param.set(Param::ServerFolder, server_folder.as_ref());
|
||||
param.set_int(Param::ServerUid, server_uid as i32);
|
||||
if self.has_chat_version() && context.get_config_bool(Config::MvboxMove) {
|
||||
param.set_int(Param::AlsoMove, 1);
|
||||
}
|
||||
job_add(context, Action::MarkseenMdnOnImap, 0, param, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -869,14 +837,9 @@ fn update_gossip_peerstates(
|
||||
|
||||
if let Ok(ref header) = gossip_header {
|
||||
if recipients.is_none() {
|
||||
recipients = Some(get_recipients(mail.headers.iter().filter_map(|v| {
|
||||
let key = v.get_key();
|
||||
let value = v.get_value();
|
||||
if key.is_err() || value.is_err() {
|
||||
return None;
|
||||
}
|
||||
Some((v.get_key().unwrap(), v.get_value().unwrap()))
|
||||
})));
|
||||
recipients = Some(get_recipients(
|
||||
mail.headers.iter().map(|v| (v.get_key(), v.get_value())),
|
||||
));
|
||||
}
|
||||
|
||||
if recipients
|
||||
@@ -920,14 +883,15 @@ pub(crate) struct Report {
|
||||
additional_message_ids: Vec<String>,
|
||||
}
|
||||
|
||||
fn parse_message_id(field: &str) -> Option<String> {
|
||||
if let Ok(addrs) = mailparse::addrparse(field) {
|
||||
// Assume the message id is a single id in the form of <id>
|
||||
if let mailparse::MailAddr::Single(mailparse::SingleInfo { ref addr, .. }) = addrs[0] {
|
||||
return Some(addr.clone());
|
||||
}
|
||||
pub(crate) fn parse_message_id(value: &str) -> crate::error::Result<String> {
|
||||
let ids = mailparse::msgidparse(value)
|
||||
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
|
||||
|
||||
if let Some(id) = ids.first() {
|
||||
Ok(id.to_string())
|
||||
} else {
|
||||
bail!("could not parse message_id: {}", value);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn is_known(key: &str) -> bool {
|
||||
@@ -990,56 +954,56 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
|
||||
}
|
||||
|
||||
fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
|
||||
if let Ok(ct) = mail.get_content_disposition() {
|
||||
return ct.disposition == DispositionType::Attachment
|
||||
&& ct
|
||||
.params
|
||||
.iter()
|
||||
.any(|(key, _value)| key.starts_with("filename"));
|
||||
}
|
||||
|
||||
false
|
||||
let ct = mail.get_content_disposition();
|
||||
ct.disposition == DispositionType::Attachment
|
||||
&& ct
|
||||
.params
|
||||
.iter()
|
||||
.any(|(key, _value)| key.starts_with("filename"))
|
||||
}
|
||||
|
||||
fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<String> {
|
||||
/// Tries to get attachment filename.
|
||||
///
|
||||
/// If filename is explitictly specified in Content-Disposition, it is
|
||||
/// returned. If Content-Disposition is "attachment" but filename is
|
||||
/// not specified, filename is guessed. If Content-Disposition cannot
|
||||
/// be parsed, returns an error.
|
||||
fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<Option<String>> {
|
||||
// try to get file name from
|
||||
// `Content-Disposition: ... filename*=...`
|
||||
// or `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...`
|
||||
// or `Content-Disposition: ... filename=...`
|
||||
|
||||
let ct = mail.get_content_disposition()?;
|
||||
ensure!(
|
||||
ct.disposition == DispositionType::Attachment,
|
||||
"disposition not an attachment: {:?}",
|
||||
ct.disposition
|
||||
);
|
||||
let ct = mail.get_content_disposition();
|
||||
|
||||
let mut desired_filename = ct
|
||||
let desired_filename: Option<String> = ct
|
||||
.params
|
||||
.iter()
|
||||
.filter(|(key, _value)| key.starts_with("filename"))
|
||||
.fold(String::new(), |mut acc, (_key, value)| {
|
||||
acc += value;
|
||||
acc
|
||||
.fold(None, |acc, (_key, value)| {
|
||||
if let Some(acc) = acc {
|
||||
Some(acc + value)
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
}
|
||||
});
|
||||
|
||||
if desired_filename.is_empty() {
|
||||
if let Some(param) = ct.params.get("name") {
|
||||
// might be a wrongly encoded filename
|
||||
desired_filename = param.to_string();
|
||||
}
|
||||
}
|
||||
let desired_filename =
|
||||
desired_filename.or_else(|| ct.params.get("name").map(|s| s.to_string()));
|
||||
|
||||
// if there is still no filename, guess one
|
||||
if desired_filename.is_empty() {
|
||||
// If there is no filename, but part is an attachment, guess filename
|
||||
if ct.disposition == DispositionType::Attachment && desired_filename.is_none() {
|
||||
if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
|
||||
desired_filename = format!("file.{}", subtype,);
|
||||
Ok(Some(format!("file.{}", subtype,)))
|
||||
} else {
|
||||
bail!("could not determine filename: {:?}", ct.disposition);
|
||||
bail!(
|
||||
"could not determine attachment filename: {:?}",
|
||||
ct.disposition
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Ok(desired_filename)
|
||||
}
|
||||
|
||||
Ok(desired_filename)
|
||||
}
|
||||
|
||||
// returned addresses are normalized and lowercased.
|
||||
@@ -1093,6 +1057,15 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
|
||||
impl AvatarAction {
|
||||
pub fn is_change(&self) -> bool {
|
||||
match self {
|
||||
AvatarAction::Delete => false,
|
||||
AvatarAction::Change(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_mimeparser_crash() {
|
||||
let context = dummy_context();
|
||||
@@ -1151,10 +1124,12 @@ mod tests {
|
||||
fn test_get_attachment_filename() {
|
||||
let raw = include_bytes!("../test-data/message/html_attach.eml");
|
||||
let mail = mailparse::parse_mail(raw).unwrap();
|
||||
assert!(get_attachment_filename(&mail).is_err());
|
||||
assert!(get_attachment_filename(&mail.subparts[0]).is_err());
|
||||
assert!(get_attachment_filename(&mail).unwrap().is_none());
|
||||
assert!(get_attachment_filename(&mail.subparts[0])
|
||||
.unwrap()
|
||||
.is_none());
|
||||
let filename = get_attachment_filename(&mail.subparts[1]).unwrap();
|
||||
assert_eq!(filename, "test.html")
|
||||
assert_eq!(filename, Some("test.html".to_string()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1240,29 +1215,29 @@ mod tests {
|
||||
|
||||
let raw = include_bytes!("../test-data/message/mail_attach_txt.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap();
|
||||
assert_eq!(mimeparser.user_avatar, AvatarAction::None);
|
||||
assert_eq!(mimeparser.group_avatar, AvatarAction::None);
|
||||
assert_eq!(mimeparser.user_avatar, None);
|
||||
assert_eq!(mimeparser.group_avatar, None);
|
||||
|
||||
let raw = include_bytes!("../test-data/message/mail_with_user_avatar.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
|
||||
assert!(mimeparser.user_avatar.is_change());
|
||||
assert_eq!(mimeparser.group_avatar, AvatarAction::None);
|
||||
assert!(mimeparser.user_avatar.unwrap().is_change());
|
||||
assert_eq!(mimeparser.group_avatar, None);
|
||||
|
||||
let raw = include_bytes!("../test-data/message/mail_with_user_avatar_deleted.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
|
||||
assert_eq!(mimeparser.user_avatar, AvatarAction::Delete);
|
||||
assert_eq!(mimeparser.group_avatar, AvatarAction::None);
|
||||
assert_eq!(mimeparser.user_avatar, Some(AvatarAction::Delete));
|
||||
assert_eq!(mimeparser.group_avatar, None);
|
||||
|
||||
let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
|
||||
assert!(mimeparser.user_avatar.is_change());
|
||||
assert!(mimeparser.group_avatar.is_change());
|
||||
assert!(mimeparser.user_avatar.unwrap().is_change());
|
||||
assert!(mimeparser.group_avatar.unwrap().is_change());
|
||||
|
||||
// if the Chat-User-Avatar header is missing, the avatar become a normal attachment
|
||||
let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml");
|
||||
@@ -1271,8 +1246,8 @@ mod tests {
|
||||
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw.as_bytes()).unwrap();
|
||||
assert_eq!(mimeparser.parts.len(), 1);
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::Image);
|
||||
assert_eq!(mimeparser.user_avatar, AvatarAction::None);
|
||||
assert!(mimeparser.group_avatar.is_change());
|
||||
assert_eq!(mimeparser.user_avatar, None);
|
||||
assert!(mimeparser.group_avatar.unwrap().is_change());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1358,7 +1333,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
Some("Chat: Message opened".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(message.parts.len(), 0);
|
||||
assert_eq!(message.parts.len(), 1);
|
||||
assert_eq!(message.reports.len(), 1);
|
||||
}
|
||||
|
||||
@@ -1436,7 +1411,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
Some("Chat: Message opened".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(message.parts.len(), 0);
|
||||
assert_eq!(message.parts.len(), 2);
|
||||
assert_eq!(message.reports.len(), 2);
|
||||
}
|
||||
|
||||
@@ -1481,7 +1456,7 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
|
||||
Some("Chat: Message opened".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(message.parts.len(), 0);
|
||||
assert_eq!(message.parts.len(), 1);
|
||||
assert_eq!(message.reports.len(), 1);
|
||||
assert_eq!(message.reports[0].original_message_id, "foo@example.org");
|
||||
assert_eq!(
|
||||
@@ -1491,14 +1466,38 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailparse_test() {
|
||||
let body = b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>";
|
||||
let mail = mailparse::parse_mail(body).unwrap();
|
||||
fn test_parse_inline_attachment() {
|
||||
let context = dummy_context();
|
||||
let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC)
|
||||
From: sender@example.com
|
||||
To: receiver@example.com
|
||||
Subject: Mail with inline attachment
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="----=_Part_25_46172632.1581201680436"
|
||||
|
||||
let from = mail.headers[0].get_value().unwrap();
|
||||
assert_eq!(&from, "Имя, Фамилия <foobar@example.com>");
|
||||
------=_Part_25_46172632.1581201680436
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
let parsed = mailparse::addrparse(&from).unwrap();
|
||||
assert_eq!(parsed.len(), 1);
|
||||
Hello!
|
||||
|
||||
------=_Part_25_46172632.1581201680436
|
||||
Content-Type: application/pdf; name="some_pdf.pdf"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: inline; filename="some_pdf.pdf"
|
||||
|
||||
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
|
||||
Y29kZT4+CnN0cmVhbQp4nGVOuwoCMRDs8xVbC8aZvC4Hx4Hno7ATAhZi56MTtPH33YtXiLKQ3ZnM
|
||||
MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
|
||||
------=_Part_25_46172632.1581201680436--
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
|
||||
assert_eq!(
|
||||
message.get_subject(),
|
||||
Some("Mail with inline attachment".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(message.parts.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
@@ -316,6 +316,25 @@ impl<'a> Peerstate<'a> {
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -326,7 +345,15 @@ impl<'a> Peerstate<'a> {
|
||||
let header = Aheader::new(
|
||||
self.addr.clone(),
|
||||
public_key,
|
||||
EncryptPreference::NoPreference,
|
||||
// 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 {
|
||||
|
||||
28
src/pgp.rs
28
src/pgp.rs
@@ -16,6 +16,7 @@ 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::*;
|
||||
@@ -147,10 +148,18 @@ pub struct KeyPair {
|
||||
}
|
||||
|
||||
/// Create a new key pair.
|
||||
pub(crate) fn create_keypair(addr: EmailAddress) -> std::result::Result<KeyPair, PgpKeygenError> {
|
||||
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)
|
||||
@@ -173,7 +182,7 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> std::result::Result<KeyPair,
|
||||
])
|
||||
.subkey(
|
||||
SubkeyParamsBuilder::default()
|
||||
.key_type(PgpKeyType::Rsa(2048))
|
||||
.key_type(public_key_type)
|
||||
.can_encrypt(true)
|
||||
.passphrase(None)
|
||||
.build()
|
||||
@@ -385,10 +394,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // is too expensive
|
||||
fn test_create_keypair() {
|
||||
let keypair0 = create_keypair(EmailAddress::new("foo@bar.de").unwrap()).unwrap();
|
||||
let keypair1 = create_keypair(EmailAddress::new("two@zwo.de").unwrap()).unwrap();
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,16 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
};
|
||||
|
||||
// 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,
|
||||
@@ -65,6 +75,16 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
};
|
||||
|
||||
// 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,
|
||||
@@ -165,12 +185,45 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
};
|
||||
|
||||
// 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,
|
||||
@@ -229,15 +282,14 @@ lazy_static::lazy_static! {
|
||||
],
|
||||
};
|
||||
|
||||
// zoho.com.md: zoho.com
|
||||
// - skipping provider with status OK and no special things to do
|
||||
|
||||
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),
|
||||
@@ -260,6 +312,11 @@ lazy_static::lazy_static! {
|
||||
("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),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! [Provider database](https://providers.delta.chat/) module
|
||||
|
||||
mod data;
|
||||
|
||||
use crate::dc_tools::EmailAddress;
|
||||
|
||||
@@ -770,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);
|
||||
|
||||
@@ -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::*;
|
||||
@@ -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.
|
||||
@@ -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!(
|
||||
|
||||
38
src/sql.rs
38
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),
|
||||
};
|
||||
@@ -360,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);
|
||||
@@ -893,6 +893,11 @@ fn open(
|
||||
)?;
|
||||
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)
|
||||
@@ -1048,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;
|
||||
@@ -1160,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.",);
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
xsBNBF425W8BCADLIbltPzG1vk/V2ov2+eBeJJJnRu1kJHdo6e3oNB+HTIxVde5+7Uq8tTEDZB1O7m9NBUFrXr7UYQsA/86G2jmsyWKTzIu1O/t5kdcNDqsNcTVZAhBu2ixYsYVc3ws6kJONjpXLtD2u3P7vEXU3INiOb2JrBQDT8/ubEm1xas/UirYnP5DMaH068IHRdVEYs9ULFaD5scw1m/94buXYZ1CRt/2hT8iRrtBi6ki8kArnhsZC2Xr0+jRQNMUnG5k7Bwi6saCqVmd7IlqSM6MbfYank30Gi/UyDmyIrOk7daTg6WIqgiVOTHav65EK/aUvvjlr+awM+C+u35rQytzyTitZABEBAAHNEzxhbGljZUBleGFtcGxlLmNvbT7CwIkEEAEIADMCGQEFAl425ZQCGwMECwkIBwYVCAkKCwIDFgIBFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HiZQf+PLDxzWchkHAdQFbxxtoXj66aiknofjlRWHDWvUG4nULZ15tjDjnv3z22Meldr8kSV4r1+ejhLFHou9gTzAYk7eAxiybDd8AJOdK+ZgK/Nn7xjdO+HTZLhNdi+R7EektDyf8WDNktEaS8pZc74VKu4984ESi4PoqVxqGHRiSisH4cw4b2pQYxp32BkIdil7sWnqRUEoCpMoKdw2h0N7/lm+rS7/JR9cdjXaVzy1dYTqAVsTL1FTGy4osOKGOyQbkP+Cm6uNq7kC/Bt+fefsb+c2JycmI1uwdvnG7PoFslKv3lRnfkNSmrcIYlJHUl5z0yAgliophr5fqMfzQpO4zMc87ATQReNuVvAQgAuNjE1i+g4v25UNDPIMgXODU4WztE30074gQs5sZa0DQnDUMsdWc2g1o060YZDojMYJQAtBjlW1Dz8FEE7WsLNohGtRyUWmIgNxE5CpodjpwIZ0MdO4Aji0YM+g+WsOSS8kiHMs+dMFfQJuNKjujGFaMIciSaMMrUmPtzkQ/o8NEJs2Aftw90fpVR+M7Mue3++rcEX09ntbgqkgm8SV6OIrOY2kfILudtybocgYkCTeNVqz5VFXuxrnT4ceyFQ64JkwsZxb+X/pCm4V5Q2TbKRwtdonU8HfAz0nAd5tsNeGmf/dPLOKBCxlNEme399YmzWrT+kJBp7CIH5jlWQKyuLwARAQABwsB2BBgBCAAgBQJeNuWUAhsMFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HrEgf/Xu8eRPPdskwtyd98y64teidBpkHuIjuZKJpNyy2HhdGXQwYbNIzwINg0EJ+u2nkreNF/h2Lu+/saqI8Dai02dpYXjvxJIlCgP2os7sNhVaZSaS4XmmJjkHCfZuIKblZypKDJVc5AceZxrtvUbgG+94+H3zeRWVAA30S5ep6YPvxigvhmQah/sdzY7708/jd9uXcCbkP47PBaXCpuPiYLb3t7z8mOteJb7LOZUmSI1efiLDLTGj7ofkdDfA7E6/nF/1+nq+UIDWqljwiUzeNIJsFlZRa/9/uDEjcQbaDe9/knBs7k9pEDZX5u8SSwSED75L+OvRpFWenp4SSKvd2BUw==
|
||||
mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4MCyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDrRuI8A/8tEEXAA7g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeIeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg==
|
||||
@@ -1 +1 @@
|
||||
xcLYBF425W8BCADLIbltPzG1vk/V2ov2+eBeJJJnRu1kJHdo6e3oNB+HTIxVde5+7Uq8tTEDZB1O7m9NBUFrXr7UYQsA/86G2jmsyWKTzIu1O/t5kdcNDqsNcTVZAhBu2ixYsYVc3ws6kJONjpXLtD2u3P7vEXU3INiOb2JrBQDT8/ubEm1xas/UirYnP5DMaH068IHRdVEYs9ULFaD5scw1m/94buXYZ1CRt/2hT8iRrtBi6ki8kArnhsZC2Xr0+jRQNMUnG5k7Bwi6saCqVmd7IlqSM6MbfYank30Gi/UyDmyIrOk7daTg6WIqgiVOTHav65EK/aUvvjlr+awM+C+u35rQytzyTitZABEBAAEAB/oDQFnwdrd7+jza5nGhFWTS/PDe+FKqbK8AneXx9ouepcoFQCr+Gxw8IwZS0JJrhgOADxp59n1FdvwvGukaXXnY2yxZw0dlMj2XN49ipR51y58X+qF6tMFK9iR1VRif6lqCRIr/RLZMCzuFZhkjNcJhnUTNA7p8qgYX+FaKHzSOaVat/v0kIUHUcZDkREWPUESYDmc1Nv6FXhB0WBiTsBglF+fq5Rm7UWPSmA59Cr7BrW8DctbzTh0+6bkzum2xdOcZ59nuTZa+IKcReI1+kVne5JPNFNJ2tP2f9GSSlL7u+NBtx3zRxZgAotXcJK9cVNIWtegqf+2hoLvm7m2CkWKRBADglpC7TpjV+8wJH+KuyGQ7jepqzf5EHwMrK2i6lPnnmoi0nkKvkklvtdcC7FoFGtLCDJ7vwlUdeN+itDxPlP8bbbUabcy0lLuzyGOVt5NwYXgIuPicpdt2ZTJgvChd9oWi1DG8pVpm+EMJZPyYVEpvDGl6q95oktrytbqjASZbBQQA54roJnwBcptLMTrttDrglULX7ciSKY5HXN1c0rqZn1dTKB1nPYB26hNbu6lZ8ixSOyZm3KwpeDUNW7A3hyzXOfoGFPaddH6WMSFFsGGC/orRVxnuPZLr3UJ3uFX7J0JOav90n/6A4YmS7uImRAG4/vTrAbEfmlBl5msHVUaYh0UD/jSX22JLenO1o8pNU04JQl3lQ4mWY6MvgTyCvpchTzDDva+wdOBTUeVUmb/KqYkYBq98tXl1VnGnNpeEymUISSi60RjaXDhbg7a3ELV0yvvWcBN9zreyyINuCU5OmNefPRvPt4Co12KtIxPACByFNTevzPKbrXd1cyhHOxAuqfzLRbDNEzxhbGljZUBleGFtcGxlLmNvbT7CwIkEEAEIADMCGQEFAl425ZQCGwMECwkIBwYVCAkKCwIDFgIBFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HiZQf+PLDxzWchkHAdQFbxxtoXj66aiknofjlRWHDWvUG4nULZ15tjDjnv3z22Meldr8kSV4r1+ejhLFHou9gTzAYk7eAxiybDd8AJOdK+ZgK/Nn7xjdO+HTZLhNdi+R7EektDyf8WDNktEaS8pZc74VKu4984ESi4PoqVxqGHRiSisH4cw4b2pQYxp32BkIdil7sWnqRUEoCpMoKdw2h0N7/lm+rS7/JR9cdjXaVzy1dYTqAVsTL1FTGy4osOKGOyQbkP+Cm6uNq7kC/Bt+fefsb+c2JycmI1uwdvnG7PoFslKv3lRnfkNSmrcIYlJHUl5z0yAgliophr5fqMfzQpO4zMc8fC2AReNuVvAQgAuNjE1i+g4v25UNDPIMgXODU4WztE30074gQs5sZa0DQnDUMsdWc2g1o060YZDojMYJQAtBjlW1Dz8FEE7WsLNohGtRyUWmIgNxE5CpodjpwIZ0MdO4Aji0YM+g+WsOSS8kiHMs+dMFfQJuNKjujGFaMIciSaMMrUmPtzkQ/o8NEJs2Aftw90fpVR+M7Mue3++rcEX09ntbgqkgm8SV6OIrOY2kfILudtybocgYkCTeNVqz5VFXuxrnT4ceyFQ64JkwsZxb+X/pCm4V5Q2TbKRwtdonU8HfAz0nAd5tsNeGmf/dPLOKBCxlNEme399YmzWrT+kJBp7CIH5jlWQKyuLwARAQABAAf/YmpfWp5fLZvjJ8kVDqIZ4r5LNB+5Sp7nbC3G7lPblBDAXgpOyG9ckdDcbguTWa6yChWizkCXFOhkCKZKVlHw1Wb3JoSB5CFsf4U29pMZe41N2BTeoohV5Fg2nojgNWxtZHwDJ6VsTonidGH9l1sN5AU6gPNF+QZ07MKsRCbRYi0yMgX064gwZXRtkm8AECz8ay1wDzoBy14ALe9aDClafVwfxdYUcxDBqtvjLhGeTWX5lMMAQ1Ix8D0Gp4r0Zvtl+oxlTSZFAt9m6sbRBbJf4LJjRQh07aWF2gUOiyIyz7YymYdwsyFnCPn2Aj84uRdqYCekAUfzBeNTBukUQq1DYQQA3BeH38pnr34m0UyD/tCrTvrX60MOJVvFuaTQw+IgY4XmT9UiiiqYMaoLfzxeevdMCQ9EtMdXUTjI27/II3dR5Obg6J0QTybj78IKPbH8Vdlg0etllRjC3bV/M4a5UcXPKG6W5CvB0UJg7eqn/8wUqwiL9x+hZoLy+nU5rCAjzZEEANcBK8Vy9eBxkKmEfH/mChDSKE82ua0xdQuZiTvvGedUYG3ucH4rAlkZaZcZrtJTod1BKhAhDBrjxk/yLCjK3z5JDsacdDGGfaqga3zdPBJubWE7f4mg6uYVs04Uf90YVY7t0LEQAh7i9QYiIqUOJDy3L8y3+bNgNz2r1p8pFd+/A/0YoYE0YDgABbLKmBQFoWjF3Op7P+k6Z4ENK4Me1fkNSAU451QX7ZduI7i3pGTM06bXG2umhTI1lg48ZveMRk1vBezHU+ThnciEkuhYafnq7NRdkEtI20MyFmN7dZF8LQ/joYKsJbeSG5svj8f1ue2eHkiIIlTtDqVUTizDU3ddlzUZwsB2BBgBCAAgBQJeNuWUAhsMFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HrEgf/Xu8eRPPdskwtyd98y64teidBpkHuIjuZKJpNyy2HhdGXQwYbNIzwINg0EJ+u2nkreNF/h2Lu+/saqI8Dai02dpYXjvxJIlCgP2os7sNhVaZSaS4XmmJjkHCfZuIKblZypKDJVc5AceZxrtvUbgG+94+H3zeRWVAA30S5ep6YPvxigvhmQah/sdzY7708/jd9uXcCbkP47PBaXCpuPiYLb3t7z8mOteJb7LOZUmSI1efiLDLTGj7ofkdDfA7E6/nF/1+nq+UIDWqljwiUzeNIJsFlZRa/9/uDEjcQbaDe9/knBs7k9pEDZX5u8SSwSED75L+OvRpFWenp4SSKvd2BUw==
|
||||
lFgEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5AAAQDMpCY4sD5/DUR0jRjGC5WstwShz1q+5Vofo5mY9+XRXRA3tBlBbGljZSA8YWxpY2VAZXhhbXBsZS5vcmc+iJAEExYIADgWIQQub6LLI7Uy1yhjS1hksI9hqe2UQwUCXlh13QIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBksI9hqe2UQxN6AP4uDAsgzE7I1qg2aEMF/wPxqEztv1dMCj4YyScv/JtqTAD5AYzpxjrcdkmaULyRk2vYfNOEWOog60biPAP/LRBFwAOcXQReWHXdEgorBgEEAZdVAQUBAQdABu3I1stkhQFPCp5bZbm1Vuu6xYsn6dNSa0Xul6stth0DAQgHAAD/X9y9I/JFBeArkgR3U363cWXXxMCWftS+BDwM9zE4PrgQb4h4BBgWCAAgFiEELm+iyyO1MtcoY0tYZLCPYantlEMFAl5Ydd0CGwwACgkQZLCPYantlEMujwEA5sTwaewZXArM2oK8d5aAmyqGNLcLqC9KVXe0Sb1eYXoBANe5wjJV+gHCjIyHaHpxKf8BOflAlvfmmCj6K+neOwwC
|
||||
@@ -44,9 +44,9 @@ fn stress_functions(context: &Context) {
|
||||
// assert!(dc_is_configured(context) != 0, "Missing configured context");
|
||||
|
||||
// let setupcode = dc_create_setup_code(context);
|
||||
// let setupcode_c = CString::yolo(setupcode.clone());
|
||||
// let setupcode_c = CString::new(setupcode.clone()).unwrap();
|
||||
// let setupfile = dc_render_setup_file(context, &setupcode).unwrap();
|
||||
// let setupfile_c = CString::yolo(setupfile);
|
||||
// let setupfile_c = CString::new(setupfile).unwrap();
|
||||
// let mut headerline_2: *const libc::c_char = ptr::null();
|
||||
// let payload = dc_decrypt_setup_file(context, setupcode_c.as_ptr(), setupfile_c.as_ptr());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user