mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
138 Commits
test-ecc-b
...
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 | ||
|
|
13e361aabc | ||
|
|
d1a26e66a7 | ||
|
|
ffe3c84e7c | ||
|
|
702c7382a7 | ||
|
|
b138d486e4 |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,33 @@
|
||||
# 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
|
||||
|
||||
566
Cargo.lock
generated
566
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.25.0"
|
||||
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 = "0.10.2"
|
||||
mailparse = "0.12.0"
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
native-tls = "0.2.3"
|
||||
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
|
||||
pretty_env_logger = "0.3.1"
|
||||
zip = "0.5"
|
||||
|
||||
rustyline = { version = "4.1.0", optional = true }
|
||||
|
||||
@@ -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.25.0"
|
||||
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"]
|
||||
|
||||
@@ -373,6 +373,14 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `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().
|
||||
*
|
||||
@@ -901,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
|
||||
|
||||
|
||||
/**
|
||||
@@ -939,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)
|
||||
@@ -1292,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.
|
||||
@@ -1655,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()
|
||||
@@ -4124,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
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -433,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();
|
||||
@@ -1045,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,
|
||||
@@ -1455,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
|
||||
@@ -2406,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()
|
||||
@@ -2728,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()
|
||||
@@ -2747,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())
|
||||
@@ -2758,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()
|
||||
@@ -2768,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3098,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()
|
||||
@@ -3108,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()
|
||||
@@ -3120,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()
|
||||
@@ -3132,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()
|
||||
@@ -3144,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()
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::ptr;
|
||||
/// # 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);
|
||||
@@ -17,7 +17,7 @@ use std::ptr;
|
||||
/// 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);
|
||||
@@ -32,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,
|
||||
@@ -66,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
|
||||
@@ -131,15 +131,16 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +152,7 @@ impl CStringExt for CString {}
|
||||
/// 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
|
||||
@@ -168,35 +169,44 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
// 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 [StrExt] trait but a different trait name
|
||||
/// to work around the type system not allowing to implement [StrExt]
|
||||
/// for `Option<impl StrExt>` When we already have an [StrExt] impl
|
||||
/// 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
|
||||
/// [StrExt::strdup], when it is [Option::None] a null pointer is
|
||||
/// [Strdup::strdup], when it is [Option::None] a null pointer is
|
||||
/// returned.
|
||||
pub trait OptStrExt {
|
||||
pub(crate) trait OptStrdup {
|
||||
/// Allocate a new raw C `*char` version of this string, or NULL.
|
||||
///
|
||||
/// See [StrExt::strdup] for details.
|
||||
/// See [Strdup::strdup] for details.
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> OptStrExt for Option<T> {
|
||||
impl<T: AsRef<str>> OptStrdup for Option<T> {
|
||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||
match self {
|
||||
Some(s) => {
|
||||
let tmp = CString::yolo(s.as_ref());
|
||||
let tmp = CString::new_lossy(s.as_ref());
|
||||
dc_strdup(tmp.as_ptr())
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
@@ -204,7 +214,7 @@ impl<T: AsRef<str>> OptStrExt for Option<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||
pub(crate) fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||
if s.is_null() {
|
||||
return "".into();
|
||||
}
|
||||
@@ -214,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;
|
||||
}
|
||||
@@ -235,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 {
|
||||
@@ -247,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)
|
||||
}
|
||||
|
||||
@@ -354,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]
|
||||
|
||||
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,5 +1,6 @@
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
|
||||
use deltachat::chatlist::*;
|
||||
@@ -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;
|
||||
@@ -374,6 +376,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
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\
|
||||
@@ -397,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\
|
||||
@@ -867,6 +871,24 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
|
||||
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()?);
|
||||
@@ -1028,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");
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ const DB_COMMANDS: [&str; 11] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 26] = [
|
||||
const CHAT_COMMANDS: [&str; 27] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
@@ -289,6 +289,7 @@ const CHAT_COMMANDS: [&str; 26] = [
|
||||
"pin",
|
||||
"unpin",
|
||||
"delchat",
|
||||
"export-chat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
||||
"listmsgs",
|
||||
@@ -308,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 {
|
||||
|
||||
@@ -76,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):
|
||||
@@ -1366,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):
|
||||
|
||||
@@ -14,6 +14,7 @@ passenv =
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
DCC_PY_LIVECONFIG
|
||||
DCC_NEW_TMP_EMAIL
|
||||
CARGO_TARGET_DIR
|
||||
RUSTC_WRAPPER
|
||||
deps =
|
||||
|
||||
@@ -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) {
|
||||
|
||||
86
src/chat.rs
86
src/chat.rs
@@ -286,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,
|
||||
}
|
||||
}
|
||||
@@ -362,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
|
||||
@@ -1450,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<_>| {
|
||||
@@ -1584,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,
|
||||
@@ -2063,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() {
|
||||
@@ -2093,10 +2162,9 @@ pub fn remove_contact_from_chat(
|
||||
});
|
||||
}
|
||||
}
|
||||
if remove_from_chat_contacts_table(context, chat_id, contact_id) {
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
success = true;
|
||||
}
|
||||
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! # Chat list module
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::*;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
@@ -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`
|
||||
@@ -139,6 +157,9 @@ impl Chatlist {
|
||||
)?
|
||||
} 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
|
||||
@@ -200,21 +228,23 @@ impl Chatlist {
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1))
|
||||
WHERE c.id>9
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND c.blocked=0
|
||||
AND NOT c.archived=?2
|
||||
AND NOT c.archived=?3
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft, ChatVisibility::Archived, ChatVisibility::Pinned],
|
||||
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;
|
||||
}
|
||||
@@ -399,6 +429,25 @@ mod tests {
|
||||
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();
|
||||
@@ -421,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;
|
||||
@@ -65,6 +68,25 @@ pub enum Config {
|
||||
#[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,
|
||||
@@ -128,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<()> {
|
||||
@@ -171,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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ pub(crate) 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 {
|
||||
|
||||
@@ -80,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;
|
||||
|
||||
@@ -213,6 +214,9 @@ pub const DC_BOB_SUCCESS: i32 = 1;
|
||||
// max. width/height of an avatar
|
||||
pub const AVATAR_SIZE: u32 = 192;
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -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,
|
||||
@@ -497,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 {
|
||||
@@ -1058,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'\''
|
||||
@@ -1167,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]
|
||||
@@ -1229,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
|
||||
);
|
||||
|
||||
@@ -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};
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -340,11 +347,9 @@ fn add_parts(
|
||||
};
|
||||
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(
|
||||
@@ -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;
|
||||
@@ -1023,6 +1055,17 @@ fn create_or_lookup_group(
|
||||
// add members to group/check members
|
||||
if recreate_member_list {
|
||||
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();
|
||||
|
||||
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF);
|
||||
}
|
||||
if from_id > DC_CONTACT_ID_LAST_SPECIAL
|
||||
@@ -1477,6 +1520,7 @@ 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: &str) -> bool {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
@@ -1523,6 +1567,7 @@ pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -
|
||||
|
||||
/// Check if a message is a reply to any messenger message.
|
||||
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(
|
||||
@@ -1612,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() {
|
||||
@@ -1671,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,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)
|
||||
@@ -417,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";
|
||||
@@ -429,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")]
|
||||
@@ -52,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())
|
||||
}
|
||||
}
|
||||
@@ -79,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);
|
||||
}
|
||||
}
|
||||
|
||||
141
src/imap/mod.rs
141
src/imap/mod.rs
@@ -188,7 +188,6 @@ struct ImapConfig {
|
||||
/// True if the server has MOVE capability as defined in
|
||||
/// https://tools.ietf.org/html/rfc6851
|
||||
pub can_move: bool,
|
||||
pub imap_delimiter: char,
|
||||
}
|
||||
|
||||
impl Default for ImapConfig {
|
||||
@@ -206,7 +205,6 @@ impl Default for ImapConfig {
|
||||
selected_folder_needs_expunge: false,
|
||||
can_idle: false,
|
||||
can_move: false,
|
||||
imap_delimiter: '.',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -796,7 +794,6 @@ impl Imap {
|
||||
folder: &str,
|
||||
uid: u32,
|
||||
dest_folder: &str,
|
||||
dest_uid: &mut u32,
|
||||
) -> ImapActionResult {
|
||||
task::block_on(async move {
|
||||
if folder == dest_folder {
|
||||
@@ -813,10 +810,6 @@ 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);
|
||||
|
||||
@@ -996,10 +989,10 @@ impl Imap {
|
||||
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
|
||||
@@ -1021,7 +1014,7 @@ impl Imap {
|
||||
display_imap_id,
|
||||
message_id,
|
||||
);
|
||||
return ImapActionResult::Failed;
|
||||
return ImapActionResult::AlreadyDone;
|
||||
};
|
||||
|
||||
let remote_message_id = get_fetch_headers(fetch)
|
||||
@@ -1036,7 +1029,7 @@ impl Imap {
|
||||
remote_message_id,
|
||||
message_id,
|
||||
);
|
||||
*uid = 0;
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1044,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,
|
||||
@@ -1074,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);
|
||||
@@ -1104,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
|
||||
@@ -1168,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(())
|
||||
@@ -1205,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;
|
||||
}
|
||||
@@ -1231,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
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1284,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 bcc-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 {
|
||||
@@ -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)? {
|
||||
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)?;
|
||||
|
||||
@@ -756,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"));
|
||||
}
|
||||
|
||||
|
||||
166
src/job.rs
166
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,
|
||||
@@ -413,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 => {
|
||||
@@ -437,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.",
|
||||
@@ -454,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 */
|
||||
@@ -512,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 */
|
||||
@@ -821,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)
|
||||
@@ -897,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.",);
|
||||
|
||||
@@ -904,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.",);
|
||||
}
|
||||
@@ -1045,9 +1097,9 @@ fn perform_job_action(context: &Context, mut job: &mut Job, thread: Thread, trie
|
||||
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::MarkseenMdnOnImap => job.MarkseenMdnOnImap(context),
|
||||
Action::MoveMsg => job.MoveMsg(context),
|
||||
Action::SendMdn => job.SendMdn(context),
|
||||
Action::ConfigureImap => JobConfigureImap(context),
|
||||
|
||||
@@ -36,6 +36,7 @@ pub mod constants;
|
||||
pub mod contact;
|
||||
pub mod context;
|
||||
mod e2ee;
|
||||
pub mod export_chat;
|
||||
mod imap;
|
||||
pub mod imex;
|
||||
#[macro_use]
|
||||
|
||||
153
src/message.rs
153
src/message.rs
@@ -4,6 +4,7 @@ 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};
|
||||
@@ -21,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;
|
||||
@@ -80,6 +85,55 @@ impl MsgId {
|
||||
self.0 == DC_MSG_ID_DAYMARKER
|
||||
}
|
||||
|
||||
/// Put message into trash chat and delete message text.
|
||||
///
|
||||
/// It means the message is deleted locally, but not on the server
|
||||
/// yet.
|
||||
pub fn trash(self, context: &Context) -> crate::sql::Result<()> {
|
||||
let chat_id = ChatId::new(DC_CHAT_ID_TRASH);
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs SET chat_id=?, txt='', txt_raw='' WHERE id=?",
|
||||
params![chat_id, self],
|
||||
)
|
||||
}
|
||||
|
||||
/// Deletes a message and corresponding MDNs from the database.
|
||||
pub fn delete_from_db(self, context: &Context) -> crate::sql::Result<()> {
|
||||
// We don't use transactions yet, so remove MDNs first to make
|
||||
// sure they are not left while the message is deleted.
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM msgs_mdns WHERE msg_id=?;",
|
||||
params![self],
|
||||
)?;
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"DELETE FROM msgs WHERE id=?;",
|
||||
params![self],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes IMAP server UID and folder from the database record.
|
||||
///
|
||||
/// It is used to avoid trying to remove the message from the
|
||||
/// server multiple times when there are multiple message records
|
||||
/// pointing to the same server UID.
|
||||
pub(crate) fn unlink(self, context: &Context) -> sql::Result<()> {
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs \
|
||||
SET server_folder='', server_uid=0 \
|
||||
WHERE id=?",
|
||||
params![self],
|
||||
)
|
||||
}
|
||||
|
||||
/// Bad evil escape hatch.
|
||||
///
|
||||
/// Avoid using this, eventually types should be cleaned up enough
|
||||
@@ -300,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());
|
||||
@@ -438,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> {
|
||||
@@ -809,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);
|
||||
@@ -946,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,
|
||||
@@ -972,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,
|
||||
@@ -1141,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.
|
||||
@@ -1340,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),
|
||||
) {
|
||||
@@ -1384,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(_) => {}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ 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;
|
||||
@@ -17,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::*;
|
||||
@@ -85,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();
|
||||
|
||||
@@ -111,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)?;
|
||||
|
||||
@@ -545,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 */
|
||||
@@ -746,16 +753,13 @@ impl MimeMessage {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -770,21 +774,13 @@ 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).ok())
|
||||
{
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map_or_else(Vec::new, |v| {
|
||||
v.split(' ')
|
||||
.filter_map(|s| parse_message_id(s).ok())
|
||||
@@ -800,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)
|
||||
@@ -828,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,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
|
||||
@@ -915,13 +887,8 @@ 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 ids.len() == 1 {
|
||||
let id = &ids[0];
|
||||
if id.starts_with('<') && id.ends_with('>') {
|
||||
Ok(id.chars().skip(1).take(id.len() - 2).collect())
|
||||
} else {
|
||||
bail!("message-ID {} is not enclosed in < and >", value);
|
||||
}
|
||||
if let Some(id) = ids.first() {
|
||||
Ok(id.to_string())
|
||||
} else {
|
||||
bail!("could not parse message_id: {}", value);
|
||||
}
|
||||
@@ -987,15 +954,12 @@ 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"))
|
||||
}
|
||||
|
||||
/// Tries to get attachment filename.
|
||||
@@ -1010,7 +974,7 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<Option<String
|
||||
// or `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...`
|
||||
// or `Content-Disposition: ... filename=...`
|
||||
|
||||
let ct = mail.get_content_disposition()?;
|
||||
let ct = mail.get_content_disposition();
|
||||
|
||||
let desired_filename: Option<String> = ct
|
||||
.params
|
||||
@@ -1369,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);
|
||||
}
|
||||
|
||||
@@ -1447,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);
|
||||
}
|
||||
|
||||
@@ -1492,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!(
|
||||
|
||||
@@ -88,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',
|
||||
|
||||
|
||||
85
src/pgp.rs
85
src/pgp.rs
@@ -394,7 +394,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // is too expensive
|
||||
fn test_create_keypair() {
|
||||
let keypair0 = create_keypair(
|
||||
EmailAddress::new("foo@bar.de").unwrap(),
|
||||
@@ -573,88 +572,4 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
}
|
||||
|
||||
fn test_encrypt_decrypt_fuzz(key_type: KeyGenType) {
|
||||
// sending messages from Alice to Bob
|
||||
let alice = create_keypair(EmailAddress::new("a@e.org").unwrap(), key_type).unwrap();
|
||||
let bob = create_keypair(EmailAddress::new("b@e.org").unwrap(), key_type).unwrap();
|
||||
|
||||
let alice_secret = Key::from(alice.secret.clone());
|
||||
let alice_public = Key::from(alice.public.clone());
|
||||
let bob_secret = Key::from(bob.secret.clone());
|
||||
let bob_public = Key::from(bob.public.clone());
|
||||
|
||||
let plain: &[u8] = b"just a test";
|
||||
|
||||
let mut encr_keyring = Keyring::default();
|
||||
encr_keyring.add_ref(&bob_public);
|
||||
|
||||
let mut decr_keyring = Keyring::default();
|
||||
decr_keyring.add_ref(&bob_secret);
|
||||
let mut validate_keyring = Keyring::default();
|
||||
validate_keyring.add_ref(&alice_public);
|
||||
|
||||
for _ in 0..1000 {
|
||||
let ctext = pk_encrypt(plain.as_ref(), &encr_keyring, Some(&alice_secret)).unwrap();
|
||||
let plain2 =
|
||||
pk_decrypt(ctext.as_ref(), &decr_keyring, &validate_keyring, None).unwrap();
|
||||
assert_eq!(plain2, plain);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_encrypt_decrypt_fuzz_rsa() {
|
||||
test_encrypt_decrypt_fuzz(KeyGenType::Rsa2048)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_fuzz_ecc() {
|
||||
test_encrypt_decrypt_fuzz(KeyGenType::Ed25519)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_fuzz_alice() -> Result<()> {
|
||||
let plain: &[u8] = b"just a test";
|
||||
let lit_msg = Message::new_literal_bytes("", plain);
|
||||
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let enc_key =
|
||||
SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
|
||||
.unwrap();
|
||||
let enc_subkey = &enc_key.public_subkeys[0];
|
||||
assert!(enc_subkey.is_encryption_key());
|
||||
let pkeys: Vec<&SignedPublicSubKey> = vec![&enc_subkey];
|
||||
|
||||
let skey = SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
|
||||
.unwrap();
|
||||
let skeys: Vec<&SignedSecretKey> = vec![&skey];
|
||||
|
||||
for _ in 0..1000 {
|
||||
let msg = lit_msg.encrypt_to_keys(
|
||||
&mut rng,
|
||||
pgp::crypto::sym::SymmetricKeyAlgorithm::AES128,
|
||||
&pkeys[..],
|
||||
)?;
|
||||
let ctext = msg.to_armored_string(None)?;
|
||||
|
||||
println!("{}", ctext);
|
||||
|
||||
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
|
||||
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
|
||||
ensure!(!msgs.is_empty(), "No valid messages found");
|
||||
|
||||
let dec_msg = &msgs[0];
|
||||
|
||||
let plain2: Vec<u8> = match dec_msg.get_content()? {
|
||||
Some(content) => content,
|
||||
None => bail!("Decrypted message is empty"),
|
||||
};
|
||||
|
||||
assert_eq!(plain2, plain);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
21
src/sql.rs
21
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::*;
|
||||
@@ -1053,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;
|
||||
@@ -1165,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.",);
|
||||
}
|
||||
|
||||
|
||||
@@ -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