Compare commits

..

1 Commits

Author SHA1 Message Date
B. Petersen
3253d427ec do not downgrade protected chats over the wire
downgrading is possible only for oneself,
not for the whole group.

enabling is still done for all as a "best effort".

this gives the user who enabled protection
strong guarantess about ones state.

downside may be different views on a chat by different users,
however, that could also happen before.
2021-10-17 14:08:11 +02:00
42 changed files with 576 additions and 1631 deletions

View File

@@ -1,42 +1,5 @@
# Changelog
## 1.63.0
### API changes
- `dc_get_last_error()` added #2788
### Changes
- Optimize Autocrypt gossip #2743
### Fixes
- fix permanently hiding of one-to-one chats after secure-join #2791
## 1.62.0
### API Changes
- `dc_join_securejoin()` now always returns immediately;
the returned chat may not allow sending (`dc_chat_can_send()` returns false)
which may change as usual on `DC_EVENT_CHAT_MODIFIED` #2508 #2767
- introduce multi-device-sync-messages;
as older cores display them as files in self-chat,
they are currently only sent if config option `send_sync_msgs` is set #2669
- add `DC_EVENT_SELFAVATAR_CHANGED` #2742
### Changes
- use system DNS instead of google for MX queries #2780
- improve error logging #2758
- improve tests #2764 #2781
- improve ci #2770
- refactorings #2677 #2728 #2740 #2729 #2766 #2778
### Fixes
- add Let's Encrypt certificate to core as it may be missing older devices #2752
- prioritize certificate setting from user over the one from provider-db #2749
- fix "QR process failed" error #2725
- do not update quota in endless loop #2726
## 1.61.0
### API Changes

51
Cargo.lock generated
View File

@@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "addr2line"
version = "0.17.0"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd"
dependencies = [
"gimli",
]
@@ -114,9 +114,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.45"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "arrayvec"
@@ -423,9 +423,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "backtrace"
version = "0.3.63"
version = "0.3.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6"
checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01"
dependencies = [
"addr2line",
"cc",
@@ -1072,7 +1072,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.63.0"
version = "1.61.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1131,7 +1131,7 @@ dependencies = [
"sha-1",
"sha2",
"smallvec",
"stop-token 0.6.1",
"stop-token 0.5.1",
"strum",
"strum_macros",
"surf",
@@ -1152,7 +1152,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.63.0"
version = "1.61.0"
dependencies = [
"anyhow",
"async-std",
@@ -1734,9 +1734,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.26.0"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81a03ce013ffccead76c11a15751231f777d9295b845cc1266ed4d34fcbd7977"
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
[[package]]
name = "gloo-timers"
@@ -2104,9 +2104,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.106"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673"
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
[[package]]
name = "libm"
@@ -2417,9 +2417,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.27.0"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c821014c18301591b89b843809ef953af9e3df0496c232d5c0611b0a52aac363"
checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2"
dependencies = [
"memchr",
]
@@ -2801,9 +2801,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
[[package]]
name = "proc-macro2"
version = "1.0.32"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
dependencies = [
"unicode-xid",
]
@@ -3324,9 +3324,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.69"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [
"itoa",
"ryu",
@@ -3566,9 +3566,9 @@ dependencies = [
[[package]]
name = "stop-token"
version = "0.6.1"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3829f0fc642e19ef8a1072ba8e33139a385f609ba56761df501c5272a9704918"
checksum = "561eec821556b41afc80e8943d59ff72e5dc9876c364f26331293f2eb6c8e461"
dependencies = [
"async-channel",
"cfg-if 1.0.0",
@@ -3614,11 +3614,10 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "surf"
version = "2.3.2"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7"
checksum = "73f856d60bdb4679fc9ec516c34093484e963431b5016a8429f85a8e74b5ccaa"
dependencies = [
"async-native-tls",
"async-std",
"async-trait",
"cfg-if 1.0.0",
@@ -3636,9 +3635,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.81"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.63.0"
version = "1.61.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -65,7 +65,7 @@ serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.9"
sha2 = "0.9"
smallvec = "1"
stop-token = "0.6"
stop-token = "0.5"
strum = "0.22"
strum_macros = "0.22"
surf = { version = "2.3", default-features = false, features = ["h1-client"] }

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.63.0"
version = "1.61.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"

View File

@@ -2249,12 +2249,21 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
* This function is typically called when dc_check_qr() returns
* lot.state=DC_QR_ASK_VERIFYCONTACT or lot.state=DC_QR_ASK_VERIFYGROUP.
*
* The function returns immediately and the handshake runs in background,
* sending and receiving several messages.
* During the handshake, info messages are added to the chat,
* showing progress, success or errors.
* Depending on the given QR code,
* this function may takes some time and sends and receives several messages.
* Therefore, you should call it always in a separate thread;
* if you want to abort it, you should call dc_stop_ongoing_process().
*
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
* - If the given QR code starts the Setup-Contact protocol,
* the function typically returns immediately
* and the handshake runs in background.
* Subsequent calls of dc_join_securejoin() will abort unfinished tasks.
* The returned chat is the one-to-one opportunistic chat.
* When the protocol has finished, an info-message is added to that chat.
* - If the given QR code starts the Verified-Group-Invite protocol,
* the function waits until the protocol has finished.
* This is because the protected group is not opportunistic
* and can be created only when the contacts have verified each other.
*
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
@@ -2264,8 +2273,10 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
* @param qr The text of the scanned QR code. Typically, the same string as given
* to dc_check_qr().
* @return Chat-id of the joined chat, the UI may redirect to the this chat.
* On errors, 0 is returned, however, most errors will happen during handshake later on.
* If the out-of-band verification failed or was aborted, 0 is returned.
* A returned chat-id does not guarantee that the chat is protected or the belonging contact is verified.
* If needed, this be checked with dc_chat_is_protected() and dc_contact_is_verified(),
* however, in practise, the UI will just listen to #DC_EVENT_CONTACTS_CHANGED unconditionally.
*/
uint32_t dc_join_securejoin (dc_context_t* context, const char* qr);
@@ -2414,22 +2425,6 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha
void dc_delete_all_locations (dc_context_t* context);
/**
* Get last error string.
*
* This is the same error string as logged via #DC_EVENT_ERROR,
* however, using this function avoids race conditions
* if the failing function is called in another thread than dc_get_next_event().
*
* @memberof dc_context_t
* @param context The context object.
* @return Last error or an empty string if there is no last error.
* NULL is never returned.
* The returned value must be released using dc_str_unref() after usage.
*/
char* dc_get_last_error (dc_context_t* context);
/**
* Release a string returned by another deltachat-core function.
* - Strings returned by any deltachat-core-function
@@ -5373,14 +5368,6 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_CONNECTIVITY_CHANGED 2100
/**
* The user's avatar changed.
* You can get the new avatar file with `dc_get_config(context, "selfavatar")`.
*/
#define DC_EVENT_SELFAVATAR_CHANGED 2110
/**
* @}
*/
@@ -5964,21 +5951,6 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by human-readable date and time.
#define DC_STR_DOWNLOAD_AVAILABILITY 100
/// "Multi Device Synchronization"
///
/// Used in subjects of outgoing sync messages.
#define DC_STR_SYNC_MSG_SUBJECT 101
/// "This message is used to synchronize data between your devices."
///
///
/// Used as message text of outgoing sync messages.
/// The text is visible in non-dc-muas or in outdated Delta Chat versions,
/// the default text therefore adds the following hint:
/// "If you see this message in Delta Chat,
/// please update your Delta Chat apps on all devices."
#define DC_STR_SYNC_MSG_BODY 102
/// "Incoming Messages"
///
/// Used as a headline in the connectivity view.
@@ -6053,26 +6025,6 @@ void dc_event_unref(dc_event_t* event);
/// Used for describing resource usage, resulting string will be eg. "1.2 GiB of 3 GiB used".
#define DC_STR_PART_OF_TOTAL_USED 116
/// "%1$s invited you to join this group. Waiting for the device of %2$s to reply…"
///
/// Added as an info-message directly after scanning a QR code for joining a group.
/// May be followed by the info-messages
/// #DC_STR_SECURE_JOIN_REPLIES, #DC_STR_CONTACT_VERIFIED and #DC_STR_MSGADDMEMBER.
///
/// `%1$s` will be replaced by name and address of the inviter,
/// `%2$s` will be replaced by the name of the inviter.
#define DC_STR_SECURE_JOIN_STARTED 117
/// "%1$s replied, waiting for being added to the group…"
///
/// Info-message on scanning a QR code for joining a group.
/// Added after #DC_STR_SECURE_JOIN_STARTED.
/// If the handshake allows to skip a step and go for #DC_STR_CONTACT_VERIFIED directly,
/// this info-message is skipped.
///
/// `%1$s` will be replaced by the name of the inviter.
#define DC_STR_SECURE_JOIN_REPLIES 118
/**
* @}
*/

View File

@@ -438,7 +438,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::Warning(_)
| EventType::Error(_)
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::ErrorSelfNotInGroup(_) => 0,
EventType::MsgsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
@@ -489,7 +488,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ImexFileWritten(_)
| EventType::MsgsNoticed(_)
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
@@ -539,7 +537,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::SecurejoinInviterProgress { .. }
| EventType::SecurejoinJoinerProgress { .. }
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
@@ -2206,16 +2203,6 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_last_error()");
return "".strdup();
}
let ctx = &*context;
block_on(ctx.get_last_error()).strdup()
}
// dc_array_t
pub type dc_array_t = dc_array::dc_array_t;

View File

@@ -386,7 +386,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
sendsticker <file> [<text>]\n\
sendfile <file> [<text>]\n\
sendhtml <file for html-part> [<text for plain-part>]\n\
sendsyncmsg\n\
videochat\n\
draft [<text>]\n\
devicemsg <text>\n\
@@ -425,7 +424,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
getqr [<chat-id>]\n\
getbadqr\n\
checkqr <qr-content>\n\
joinqr <qr-content>\n\
setqr <qr-content>\n\
providerinfo <addr>\n\
event <event-id to test>\n\
@@ -897,10 +895,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}));
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendsyncmsg" => match context.send_sync_msg().await? {
Some(msg_id) => println!("sync message sent as {}.", msg_id),
None => println!("sync message not needed."),
},
"videochat" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;

View File

@@ -167,7 +167,7 @@ const DB_COMMANDS: [&str; 10] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 35] = [
const CHAT_COMMANDS: [&str; 34] = [
"listchats",
"listarchived",
"chat",
@@ -188,7 +188,6 @@ const CHAT_COMMANDS: [&str; 35] = [
"sendimage",
"sendfile",
"sendhtml",
"sendsyncmsg",
"videochat",
"draft",
"listmedia",
@@ -224,11 +223,10 @@ const CONTACT_COMMANDS: [&str; 9] = [
"unblock",
"listblocked",
];
const MISC_COMMANDS: [&str; 11] = [
const MISC_COMMANDS: [&str; 10] = [
"getqr",
"getbadqr",
"checkqr",
"joinqr",
"event",
"fileinfo",
"clear",

View File

@@ -7,5 +7,3 @@
cc c310754465ee0261807b96fa9bcc4861ff9aa286e94667524b5960c69f9b6620 # shrinks to buf = "", approx_chars = 0, do_unwrap = false
cc 5fd8d730b0a9cdf7308ce58818ca9aefc0255c9ba2a0878944fc48d43a67315b # shrinks to buf = "𑒀ὐ¢🜀\u{1e01b}A a🟠", approx_chars = 0, do_unwrap = false
cc c6a0029a54137a4b9efc9ef2ea6d9a7dd1d60d1c937bb472b66a174618ba8013 # shrinks to buf = "𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ ", approx_chars = 0, do_unwrap = false
cc 9796807baeda701227dcdcfc9fdaa93ddd556da2bb1630381bfe2e037bee73f6 # shrinks to buf = " ꫛ®a\u{11300}a", approx_chars = 0
cc 063a4c42ac1ec9aa37af54521b210ba9cd82dcc9cc3be296ca2fedf8240072d4 # shrinks to buf = "a᪠ 0A", approx_chars = 0

View File

@@ -1,7 +0,0 @@
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0"]
[tool.setuptools_scm]
root = ".."
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
git_describe_command = "git describe --dirty --tags --long --match py-*.*"

View File

@@ -8,6 +8,13 @@ def main():
long_description = f.read()
setuptools.setup(
name='deltachat',
setup_requires=['setuptools_scm', 'cffi>=1.0.0'],
use_scm_version = {
"root": "..",
"relative_to": __file__,
'tag_regex': r'^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
'git_describe_command': "git describe --dirty --tags --long --match py-*.*",
},
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
long_description=long_description,
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',

View File

@@ -3,7 +3,7 @@
import mimetypes
import calendar
import json
from datetime import datetime, timezone
from datetime import datetime
import os
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .capi import lib, ffi
@@ -512,9 +512,8 @@ class Chat(object):
latitude=lib.dc_array_get_latitude(dc_array, i),
longitude=lib.dc_array_get_longitude(dc_array, i),
accuracy=lib.dc_array_get_accuracy(dc_array, i),
timestamp=datetime.fromtimestamp(
lib.dc_array_get_timestamp(dc_array, i),
timezone.utc
timestamp=datetime.utcfromtimestamp(
lib.dc_array_get_timestamp(dc_array, i)
),
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
)

View File

@@ -1,6 +1,6 @@
from .capi import lib
from .capi import ffi
from datetime import datetime, timezone
from datetime import datetime
def as_dc_charpointer(obj):
@@ -44,4 +44,4 @@ class DCLot:
ts = lib.dc_lot_get_timestamp(self._dc_lot)
if ts == 0:
return None
return datetime.fromtimestamp(ts, timezone.utc)
return datetime.utcfromtimestamp(ts)

View File

@@ -6,7 +6,7 @@ from . import props
from .cutil import from_dc_charpointer, as_dc_charpointer
from .capi import lib, ffi
from . import const
from datetime import datetime, timezone
from datetime import datetime
class Message(object):
@@ -170,7 +170,7 @@ class Message(object):
:returns: naive datetime.datetime() object.
"""
ts = lib.dc_msg_get_timestamp(self._dc_msg)
return datetime.fromtimestamp(ts, timezone.utc)
return datetime.utcfromtimestamp(ts)
@props.with_doc
def time_received(self):
@@ -180,7 +180,7 @@ class Message(object):
"""
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
if ts:
return datetime.fromtimestamp(ts, timezone.utc)
return datetime.utcfromtimestamp(ts)
@props.with_doc
def ephemeral_timer(self):
@@ -200,7 +200,7 @@ class Message(object):
"""
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
if ts:
return datetime.fromtimestamp(ts, timezone.utc)
return datetime.utcfromtimestamp(ts)
@property
def quoted_text(self):

View File

@@ -10,7 +10,7 @@ from deltachat.tracker import ImexTracker
from deltachat.hookspec import account_hookimpl
from deltachat.capi import ffi, lib
from deltachat.cutil import iter_array
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
@pytest.mark.parametrize("msgtext,res", [
@@ -447,7 +447,7 @@ class TestOfflineChat:
contact1.create_chat().send_text("hello")
def test_chat_message_distinctions(self, ac1, chat1):
past1s = datetime.now(timezone.utc) - timedelta(seconds=1)
past1s = datetime.utcnow() - timedelta(seconds=1)
msg = chat1.send_text("msg1")
ts = msg.time_sent
assert msg.time_received is None
@@ -1221,40 +1221,6 @@ class TestOnlineAccount:
assert not msg.is_encrypted()
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
def test_gossip_optimization(self, acfactory, lp):
"""Test that gossip timestamp is updated when someone else sends gossip,
so we don't have to send gossip ourselves.
"""
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
acfactory.introduce_each_other([ac1, ac2])
acfactory.introduce_each_other([ac2, ac3])
lp.sec("ac1 creates a group chat with ac2")
group_chat = ac1.create_group_chat("hello")
group_chat.add_contact(ac2)
msg = group_chat.send_text("hi")
# No Autocrypt gossip was sent yet.
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp == 0
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert msg.text == "hi"
lp.sec("ac2 adds ac3 to the group")
msg.chat.add_contact(ac3)
lp.sec("ac1 receives message from ac2 and updates gossip timestamp")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
# ac1 has updated the gossip timestamp even though no gossip was sent by ac1.
# ac1 does not need to send gossip because ac2 already did it.
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp == int(msg.time_sent.timestamp())
def test_gossip_encryption_preference(self, acfactory, lp):
"""Test that encryption preference of group members is gossiped to new members.
This is a Delta Chat extension to Autocrypt 1.1.0, which Autocrypt-Gossip headers
@@ -2197,7 +2163,7 @@ class TestOnlineAccount:
break # DC is done with reading messages
def test_send_receive_locations(self, acfactory, lp):
now = datetime.now(timezone.utc)
now = datetime.utcnow()
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")

View File

@@ -45,8 +45,9 @@ commands =
[testenv:doc]
changedir=doc
deps =
sphinx
breathe
# Pin dependencies to the versions which actually work with Python 3.5.
sphinx==3.4.3
breathe==4.28.0
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html

View File

@@ -621,10 +621,7 @@ mod tests {
use super::*;
use crate::{
message::Message,
test_utils::{self, TestContext},
};
use crate::{message::Message, test_utils::TestContext};
use image::Pixel;
#[async_std::test]
@@ -873,10 +870,11 @@ mod tests {
async fn test_selfavatar_in_blobdir() {
let t = TestContext::new().await;
let avatar_src = t.get_blobdir().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
File::create(&avatar_src)
.await
.unwrap()
.write_all(test_utils::AVATAR_900x900_BYTES)
.write_all(avatar_bytes)
.await
.unwrap();

View File

@@ -171,20 +171,9 @@ impl ChatId {
/// This should be used when **a user action** creates a chat 1:1, it ensures the chat
/// exists and is unblocked and scales the [`Contact`]'s origin.
pub async fn create_for_contact(context: &Context, contact_id: u32) -> Result<Self> {
ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await
}
/// Same as `create_for_contact()` with an additional `create_blocked` parameter
/// that is used in case the chat does not exist or to unblock existing chats.
/// `create_blocked` won't block already unblocked chats again.
pub(crate) async fn create_for_contact_with_blocked(
context: &Context,
contact_id: u32,
create_blocked: Blocked,
) -> Result<Self> {
let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
Some(chat) => {
if create_blocked == Blocked::Not && chat.blocked != Blocked::Not {
if chat.blocked != Blocked::Not {
chat.id.unblock(context).await?;
}
chat.id
@@ -193,10 +182,7 @@ impl ChatId {
if Contact::real_exists_by_id(context, contact_id).await?
|| contact_id == DC_CONTACT_ID_SELF
{
let chat_id =
ChatIdBlocked::get_for_contact(context, contact_id, create_blocked)
.await
.map(|chat| chat.id)?;
let chat_id = ChatId::get_for_contact(context, contact_id).await?;
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?;
chat_id
} else {
@@ -266,7 +252,7 @@ impl ChatId {
/// Updates chat blocked status.
///
/// Returns true if the value was modified.
pub(crate) async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
if self.is_special() {
bail!("ignoring setting of Block-status for {}", self);
}
@@ -304,7 +290,7 @@ impl ChatId {
self.delete(context).await?;
}
Chattype::Mailinglist => {
if self.set_blocked(context, Blocked::Yes).await? {
if self.set_blocked(context, Blocked::Manually).await? {
context.emit_event(EventType::ChatModified(self));
}
}
@@ -397,7 +383,7 @@ impl ChatId {
context.emit_event(EventType::ChatModified(self));
// make sure, the receivers will get all keys
self.reset_gossiped_timestamp(context).await?;
reset_gossiped_timestamp(context, self).await?;
Ok(())
}
@@ -857,48 +843,6 @@ impl ChatId {
pub fn to_u32(self) -> u32 {
self.0
}
pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> {
self.set_gossiped_timestamp(context, 0).await
}
/// Get timestamp of the last gossip sent in the chat.
/// Zero return value means that gossip was never sent.
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
let timestamp: Option<i64> = context
.sql
.query_get_value(
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
paramsv![self],
)
.await?;
Ok(timestamp.unwrap_or_default())
}
pub(crate) async fn set_gossiped_timestamp(
self,
context: &Context,
timestamp: i64,
) -> Result<()> {
ensure!(
!self.is_special(),
"can not set gossiped timestamp for special chats"
);
info!(
context,
"set gossiped_timestamp for chat {} to {}.", self, timestamp,
);
context
.sql
.execute(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
paramsv![timestamp, self],
)
.await?;
Ok(())
}
}
impl std::fmt::Display for ChatId {
@@ -1099,6 +1043,10 @@ impl Chat {
Ok(None)
}
pub async fn get_gossiped_timestamp(&self, context: &Context) -> Result<i64> {
get_gossiped_timestamp(context, self.id).await
}
pub async fn get_color(&self, context: &Context) -> Result<u32> {
let mut color = 0;
@@ -1131,7 +1079,7 @@ impl Chat {
name: self.name.clone(),
archived: self.visibility == ChatVisibility::Archived,
param: self.param.to_string(),
gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?,
gossiped_timestamp: self.get_gossiped_timestamp(context).await?,
is_sending_locations: self.is_sending_locations,
color: self.get_color(context).await?,
profile_image: self
@@ -1243,10 +1191,6 @@ impl Chat {
msg.param.set_int(Param::AttachGroupImage, 1);
self.param.remove(Param::Unpromoted);
self.update_param(context).await?;
// send_sync_msg() is called (usually) a moment later at Job::send_msg_to_smtp()
// when the group-creation message is actually sent though SMTP -
// this makes sure, the other devices are aware of grpid that is used in the sync-message.
context.sync_qr_code_tokens(Some(self.id)).await?;
}
// reset encrypt error state eg. for forwarding
@@ -2409,7 +2353,7 @@ pub(crate) async fn add_contact_to_chat_ex(
let contact = Contact::get_by_id(context, contact_id).await?;
let mut msg = Message::default();
chat_id.reset_gossiped_timestamp(context).await?;
reset_gossiped_timestamp(context, chat_id).await?;
/*this also makes sure, not contacts are added to special or normal chats*/
let mut chat = Chat::load_from_db(context, chat_id).await?;
@@ -2439,8 +2383,6 @@ pub(crate) async fn add_contact_to_chat_ex(
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
chat.param.remove(Param::Unpromoted);
chat.update_param(context).await?;
context.sync_qr_code_tokens(Some(chat_id)).await?;
context.send_sync_msg().await?;
}
let self_addr = context
.get_config(Config::ConfiguredAddr)
@@ -2491,6 +2433,45 @@ pub(crate) async fn add_contact_to_chat_ex(
Ok(true)
}
pub(crate) async fn reset_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<()> {
set_gossiped_timestamp(context, chat_id, 0).await
}
/// Get timestamp of the last gossip sent in the chat.
/// Zero return value means that gossip was never sent.
pub async fn get_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<i64> {
let timestamp: Option<i64> = context
.sql
.query_get_value(
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
paramsv![chat_id],
)
.await?;
Ok(timestamp.unwrap_or_default())
}
pub(crate) async fn set_gossiped_timestamp(
context: &Context,
chat_id: ChatId,
timestamp: i64,
) -> Result<()> {
ensure!(!chat_id.is_special(), "can not add member to special chats");
info!(
context,
"set gossiped_timestamp for chat #{} to {}.", chat_id, timestamp,
);
context
.sql
.execute(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
paramsv![timestamp, chat_id],
)
.await?;
Ok(())
}
pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool> {
// versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others.
// to avoid sending out previously set selfavatars unexpectedly we added this additional check.
@@ -3255,7 +3236,7 @@ mod tests {
#[async_std::test]
async fn test_self_talk() -> Result<()> {
let t = TestContext::new_alice().await;
let t = TestContext::new().await;
let chat = &t.get_self_chat().await;
assert_eq!(DC_CONTACT_ID_SELF, 1);
assert!(!chat.id.is_special());
@@ -3265,23 +3246,6 @@ mod tests {
assert!(chat.can_send(&t).await?);
assert_eq!(chat.name, stock_str::saved_messages(&t).await);
assert!(chat.get_profile_image(&t).await?.is_some());
let msg_id = send_text_msg(&t, chat.id, "foo self".to_string()).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.from_id, DC_CONTACT_ID_SELF);
assert_eq!(msg.to_id, DC_CONTACT_ID_SELF);
assert!(msg.get_showpadlock());
let sent_msg = t.pop_sent_msg().await;
let t2 = TestContext::new_alice().await;
t2.recv_msg(&sent_msg).await;
let chat = &t2.get_self_chat().await;
let msg = t2.get_last_msg_in(chat.id).await;
assert_eq!(msg.text, Some("foo self".to_string()));
assert_eq!(msg.from_id, DC_CONTACT_ID_SELF);
assert_eq!(msg.to_id, DC_CONTACT_ID_SELF);
assert!(msg.get_showpadlock());
Ok(())
}
@@ -3890,7 +3854,7 @@ mod tests {
// create contact, then blocked chat
let contact_id = Contact::create(&ctx, "", "claire@foo.de").await.unwrap();
let chat_id = ChatIdBlocked::get_for_contact(&ctx, contact_id, Blocked::Yes)
let chat_id = ChatIdBlocked::get_for_contact(&ctx, contact_id, Blocked::Manually)
.await
.unwrap()
.id;
@@ -3899,7 +3863,7 @@ mod tests {
.unwrap()
.unwrap();
assert_eq!(chat_id, chat2.id);
assert_eq!(chat2.blocked, Blocked::Yes);
assert_eq!(chat2.blocked, Blocked::Manually);
// test nonexistent contact
let found = ChatId::lookup_by_contact(&ctx, 1234).await.unwrap();
@@ -4409,38 +4373,4 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_create_for_contact_with_blocked() -> Result<()> {
let t = TestContext::new().await;
let (contact_id, _) =
Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::ManuallyCreated).await?;
// create a blocked chat
let chat_id_orig =
ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?;
assert!(!chat_id_orig.is_special());
let chat = Chat::load_from_db(&t, chat_id_orig).await?;
assert_eq!(chat.blocked, Blocked::Yes);
// repeating the call, the same chat must still be blocked
let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?;
assert_eq!(chat_id, chat_id_orig);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.blocked, Blocked::Yes);
// already created chats are unblocked if requested
let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Not).await?;
assert_eq!(chat_id, chat_id_orig);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.blocked, Blocked::Not);
// however, already created chats are not re-blocked
let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?;
assert_eq!(chat_id, chat_id_orig);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.blocked, Blocked::Not);
Ok(())
}
}

View File

@@ -363,7 +363,7 @@ pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
.sql
.count(
"SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
paramsv![Blocked::Yes, ChatVisibility::Archived],
paramsv![Blocked::Manually, ChatVisibility::Archived],
)
.await?;
Ok(count)

View File

@@ -175,11 +175,6 @@ pub enum Config {
/// 0 = no limit.
#[strum(props(default = "0"))]
DownloadLimit,
/// Send sync messages, requires `BccSelf` to be set as well.
/// In a future versions, this switch may be removed.
#[strum(props(default = "0"))]
SendSyncMsgs,
}
impl Context {
@@ -284,13 +279,13 @@ impl Context {
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
blob.recode_to_avatar_size(self).await?;
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
Ok(())
}
None => {
self.sql.set_raw_config(key, None).await?;
Ok(())
}
}
self.emit_event(EventType::SelfavatarChanged);
Ok(())
}
Config::Selfstatus => {
let def = stock_str::status_line(self).await;

View File

@@ -24,7 +24,7 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
#[repr(i8)]
pub enum Blocked {
Not = 0,
Yes = 1,
Manually = 1,
Request = 2,
}
@@ -385,7 +385,7 @@ mod tests {
// values may be written to disk and must not change
assert_eq!(Blocked::Not, Blocked::default());
assert_eq!(Blocked::Not, Blocked::from_i32(0).unwrap());
assert_eq!(Blocked::Yes, Blocked::from_i32(1).unwrap());
assert_eq!(Blocked::Manually, Blocked::from_i32(1).unwrap());
assert_eq!(Blocked::Request, Blocked::from_i32(2).unwrap());
}

View File

@@ -711,7 +711,7 @@ impl Contact {
.sql
.query_map(
"SELECT name, grpid FROM chats WHERE type=? AND blocked=?;",
paramsv![Chattype::Mailinglist, Blocked::Yes],
paramsv![Chattype::Mailinglist, Blocked::Manually],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
@@ -1371,14 +1371,11 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
#[cfg(test)]
mod tests {
use async_std::fs::File;
use async_std::io::WriteExt;
use super::*;
use crate::chat::send_text_msg;
use crate::message::Message;
use crate::test_utils::{self, TestContext};
use crate::test_utils::TestContext;
#[test]
fn test_may_be_valid_addr() {
@@ -1974,64 +1971,4 @@ CCCB 5AA9 F6E1 141C 9431
Ok(())
}
/// Tests that DC_EVENT_SELFAVATAR_CHANGED is emitted on avatar changes.
#[async_std::test]
async fn test_selfavatar_changed_event() -> Result<()> {
// Alice has two devices.
let alice1 = TestContext::new_alice().await;
let alice2 = TestContext::new_alice().await;
// Bob has one device.
let bob = TestContext::new_bob().await;
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
let avatar_src = alice1.get_blobdir().join("avatar.png");
File::create(&avatar_src)
.await?
.write_all(test_utils::AVATAR_900x900_BYTES)
.await?;
alice1
.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await?;
alice1
.evtracker
.get_matching(|e| e == EventType::SelfavatarChanged)
.await;
// Bob sends a message so that Alice can encrypt to him.
let chat = bob
.create_chat_with_contact("Alice", "alice@example.com")
.await;
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
alice1.recv_msg(&sent_msg).await;
alice2.recv_msg(&sent_msg).await;
// Alice sends a message.
let alice1_chat_id = alice1.get_last_msg().await.chat_id;
alice1_chat_id.accept(&alice1).await?;
send_text_msg(&alice1, alice1_chat_id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// The message is encrypted.
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
assert!(message.get_showpadlock());
// Alice's second device receives a copy of the outgoing message.
alice2.recv_msg(&sent_msg).await;
// Alice's second device applies the selfavatar.
assert!(alice2.get_config(Config::Selfavatar).await?.is_some());
alice2
.evtracker
.get_matching(|e| e == EventType::SelfavatarChanged)
.await;
Ok(())
}
}

View File

@@ -76,11 +76,6 @@ pub struct InnerContext {
pub(crate) id: u32,
creation_time: SystemTime,
/// The text of the last error logged and emitted as an event.
/// If the ui wants to display an error after a failure,
/// `last_error` should be used to avoid races with the event thread.
pub(crate) last_error: RwLock<String>,
}
#[derive(Debug)]
@@ -152,7 +147,6 @@ impl Context {
quota: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
last_error: RwLock::new("".to_string()),
};
let ctx = Context {
@@ -309,7 +303,6 @@ impl Context {
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
let bcc_self = self.get_config_int(Config::BccSelf).await?;
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
let prv_key_cnt = self
.sql
@@ -399,7 +392,6 @@ impl Context {
self.get_config_int(Config::KeyGenType).await?.to_string(),
);
res.insert("bcc_self", bcc_self.to_string());
res.insert("send_sync_msgs", send_sync_msgs.to_string());
res.insert("private_key_count", prv_key_cnt.to_string());
res.insert("public_key_count", pub_key_cnt.to_string());
res.insert("fingerprint", fingerprint_str);

View File

@@ -142,6 +142,8 @@ pub(crate) async fn dc_receive_imf_inner(
};
// the function returns the number of created messages in the database
let mut hidden = false;
let mut needs_delete_job = false;
let mut created_db_entries = Vec::new();
@@ -201,6 +203,7 @@ pub(crate) async fn dc_receive_imf_inner(
sent_timestamp,
rcvd_timestamp,
from_id,
&mut hidden,
seen || replace_partial_download,
is_partial_download,
&mut needs_delete_job,
@@ -212,46 +215,22 @@ pub(crate) async fn dc_receive_imf_inner(
.await
.map_err(|err| err.context("add_parts error"))?;
// Update gossiped timestamp for the chat if someone else or our other device sent
// Autocrypt-Gossip for all recipients in the chat to avoid sending Autocrypt-Gossip ourselves
// and waste traffic.
if !chat_id.is_special()
&& mime_parser
.recipients
.iter()
.all(|recipient| mime_parser.gossiped_addr.contains(&recipient.addr))
{
info!(
context,
"Received message contains Autocrypt-Gossip for all members, updating timestamp."
);
if chat_id.get_gossiped_timestamp(context).await? < sent_timestamp {
chat_id
.set_gossiped_timestamp(context, sent_timestamp)
.await?;
}
}
let insert_msg_id = if let Some((_chat_id, msg_id)) = created_db_entries.last() {
*msg_id
} else {
MsgId::new_unset()
};
save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
if let Some(ref sync_items) = mime_parser.sync_items {
if from_id == DC_CONTACT_ID_SELF {
if mime_parser.was_encrypted() {
if let Err(err) = context.execute_sync_items(sync_items).await {
warn!(context, "receive_imf cannot execute sync items: {}", err);
}
} else {
warn!(context, "sync items are not encrypted.");
}
} else {
warn!(context, "sync items not sent by self.");
}
if mime_parser.location_kml.is_some() || mime_parser.message_kml.is_some() {
save_locations(
context,
&mime_parser,
chat_id,
from_id,
insert_msg_id,
hidden,
)
.await;
}
if let Some(avatar_action) = &mime_parser.user_avatar {
@@ -416,7 +395,7 @@ pub async fn from_field_to_contact_id(
#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
async fn add_parts(
context: &Context,
mime_parser: &mut MimeMessage,
mut mime_parser: &mut MimeMessage,
imf_raw: &[u8],
incoming: bool,
incoming_origin: Origin,
@@ -427,6 +406,7 @@ async fn add_parts(
sent_timestamp: i64,
rcvd_timestamp: i64,
from_id: u32,
hidden: &mut bool,
seen: bool,
is_partial_download: Option<u32>,
needs_delete_job: &mut bool,
@@ -435,13 +415,14 @@ async fn add_parts(
fetching_existing_messages: bool,
prevent_rename: bool,
) -> Result<ChatId> {
let mut state: MessageState;
let mut chat_id = None;
let mut chat_id_blocked = Blocked::Not;
let mut incoming_origin = incoming_origin;
let parent = get_parent_message(context, mime_parser).await?;
let is_dc_message = if mime_parser.has_chat_version() {
let mut is_dc_message = if mime_parser.has_chat_version() {
MessengerMessage::Yes
} else if let Some(parent) = &parent {
match parent.is_dc_message {
@@ -453,12 +434,10 @@ async fn add_parts(
};
// incoming non-chat messages may be discarded
let location_kml_is = mime_parser.location_kml.is_some();
let is_mdn = !mime_parser.mdn_reports.is_empty();
let mut allow_creation = !is_mdn;
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
let allow_creation;
if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
&& is_dc_message == MessengerMessage::No
{
@@ -470,10 +449,8 @@ async fn add_parts(
allow_creation = false;
}
ShowEmails::AcceptedContacts => allow_creation = false,
ShowEmails::All => allow_creation = !is_mdn,
ShowEmails::All => {}
}
} else {
allow_creation = !is_mdn;
}
// check if the message introduces a new chat:
@@ -482,44 +459,42 @@ async fn add_parts(
// (of course, the user can add other chats manually later)
let to_id: u32;
let state: MessageState;
if incoming {
state = if seen || fetching_existing_messages {
MessageState::InSeen
} else {
MessageState::InFresh
};
to_id = DC_CONTACT_ID_SELF;
// Whether the message is a part of securejoin handshake that should be marked as seen
// automatically.
let securejoin_seen;
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
is_dc_message = MessengerMessage::Yes; // avoid discarding by show_emails setting
chat_id = None;
allow_creation = true;
match handle_securejoin_handshake(context, mime_parser, from_id).await {
Ok(securejoin::HandshakeMessage::Done) => {
chat_id = Some(DC_CHAT_ID_TRASH);
*hidden = true;
*needs_delete_job = true;
securejoin_seen = true;
state = MessageState::InSeen;
}
Ok(securejoin::HandshakeMessage::Ignore) => {
chat_id = Some(DC_CHAT_ID_TRASH);
securejoin_seen = true;
*hidden = true;
state = MessageState::InSeen;
}
Ok(securejoin::HandshakeMessage::Propagate) => {
// process messages as "member added" normally
securejoin_seen = false;
}
Err(err) => {
warn!(context, "Error in Secure-Join message handling: {}", err);
return Ok(DC_CHAT_ID_TRASH);
}
}
} else {
securejoin_seen = false;
}
let test_normal_chat = if from_id == 0 {
Default::default()
} else {
ChatIdBlocked::lookup_by_contact(context, from_id).await?
};
let test_normal_chat = ChatIdBlocked::lookup_by_contact(context, from_id)
.await
.unwrap_or_default();
if chat_id.is_none() && mime_parser.failure_report.is_some() {
chat_id = Some(DC_CHAT_ID_TRASH);
@@ -530,7 +505,7 @@ async fn add_parts(
// try to assign to a chat based on In-Reply-To/References:
if let Some((new_chat_id, new_chat_id_blocked)) =
lookup_chat_by_reply(context, mime_parser, &parent, from_id, to_ids).await?
lookup_chat_by_reply(context, &mime_parser, &parent, from_id, to_ids).await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
@@ -550,7 +525,7 @@ async fn add_parts(
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
context,
mime_parser,
&mut mime_parser,
sent_timestamp,
if test_normal_chat.is_none() {
allow_creation
@@ -644,7 +619,7 @@ async fn add_parts(
if chat_id.is_none() {
// try to create a normal chat
let create_blocked = if from_id == DC_CONTACT_ID_SELF {
let create_blocked = if from_id == to_id {
Blocked::Not
} else {
Blocked::Request
@@ -664,12 +639,11 @@ async fn add_parts(
}
if let Some(chat_id) = chat_id {
if chat_id_blocked != Blocked::Not {
if chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
if create_blocked == Blocked::Request && parent.is_some() {
if Blocked::Not != chat_id_blocked {
if Blocked::Not == create_blocked {
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
} else if parent.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
// the contact requests will pop up and this should be just fine.
Contact::scaleup_origin_by_id(context, from_id, Origin::IncomingReplyTo)
@@ -686,12 +660,21 @@ async fn add_parts(
}
}
state =
if seen || fetching_existing_messages || is_mdn || location_kml_is || securejoin_seen {
MessageState::InSeen
} else {
MessageState::InFresh
};
// if the chat_id is blocked,
// for unknown senders and non-delta-messages set the state to NOTICED
// to not result in a chatlist-contact-request (this would require the state FRESH)
if Blocked::Not != chat_id_blocked
&& state == MessageState::InFresh
&& !incoming_origin.is_known()
&& is_dc_message == MessengerMessage::No
&& show_emails != ShowEmails::All
{
state = MessageState::InNoticed;
} else if fetching_existing_messages && Blocked::Request == chat_id_blocked {
// The fetched existing message should be shown in the chatlist-contact-request because
// a new user won't find the contact request in the menu
state = MessageState::InFresh;
}
let is_spam = (chat_id_blocked == Blocked::Request)
&& !incoming_origin.is_known()
@@ -709,28 +692,24 @@ async fn add_parts(
state = MessageState::OutDelivered;
to_id = to_ids.get_index(0).cloned().unwrap_or_default();
let self_sent = from_id == DC_CONTACT_ID_SELF
&& to_ids.len() == 1
&& to_ids.contains(&DC_CONTACT_ID_SELF);
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
is_dc_message = MessengerMessage::Yes; // avoid discarding by show_emails setting
chat_id = None;
allow_creation = true;
match observe_securejoin_on_other_device(context, mime_parser, to_id).await {
Ok(securejoin::HandshakeMessage::Done)
| Ok(securejoin::HandshakeMessage::Ignore) => {
chat_id = Some(DC_CHAT_ID_TRASH);
*hidden = true;
}
Ok(securejoin::HandshakeMessage::Propagate) => {
// process messages as "member added" normally
chat_id = None;
}
Err(err) => {
warn!(context, "Error in Secure-Join watching: {}", err);
return Ok(DC_CHAT_ID_TRASH);
}
}
} else if mime_parser.sync_items.is_some() && self_sent {
chat_id = Some(DC_CHAT_ID_TRASH);
}
// If the message is outgoing AND there is no Received header AND it's not in the sentbox,
@@ -758,13 +737,14 @@ async fn add_parts(
// Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them
info!(context, "Email is probably just a draft (TRASH)");
chat_id = Some(DC_CHAT_ID_TRASH);
allow_creation = false;
}
if chat_id.is_none() {
// try to assign to a chat based on In-Reply-To/References:
if let Some((new_chat_id, new_chat_id_blocked)) =
lookup_chat_by_reply(context, mime_parser, &parent, from_id, to_ids).await?
lookup_chat_by_reply(context, &mime_parser, &parent, from_id, to_ids).await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
@@ -775,7 +755,7 @@ async fn add_parts(
if chat_id.is_none() {
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
context,
mime_parser,
&mut mime_parser,
sent_timestamp,
allow_creation,
Blocked::Not,
@@ -807,13 +787,16 @@ async fn add_parts(
}
if let Some(chat_id) = chat_id {
if chat_id_blocked != Blocked::Not && chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
if Blocked::Not != chat_id_blocked && Blocked::Not == create_blocked {
chat_id.unblock(context).await?;
chat_id_blocked = Blocked::Not;
}
}
}
}
let self_sent = from_id == DC_CONTACT_ID_SELF
&& to_ids.len() == 1
&& to_ids.contains(&DC_CONTACT_ID_SELF);
if chat_id.is_none() && self_sent {
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
@@ -842,10 +825,6 @@ async fn add_parts(
info!(context, "Existing non-decipherable message. (TRASH)");
}
if is_mdn {
chat_id = Some(DC_CHAT_ID_TRASH);
}
let chat_id = chat_id.unwrap_or_else(|| {
info!(context, "No chat id for message (TRASH)");
DC_CHAT_ID_TRASH
@@ -868,6 +847,8 @@ async fn add_parts(
EphemeralTimer::Disabled
};
let location_kml_is = mime_parser.location_kml.is_some();
// correct message_timestamp, it should not be used before,
// however, we cannot do this earlier as we need chat_id to be set
let in_fresh = state == MessageState::InFresh;
@@ -875,11 +856,13 @@ async fn add_parts(
// Apply ephemeral timer changes to the chat.
//
// Only apply the timer when there are visible parts (e.g., the message does not consist only
// of `location.kml` attachment). Timer changes without visible received messages may be
// confusing to the user.
if !chat_id.is_special()
&& !mime_parser.parts.is_empty()
// Only non-hidden timers are applied. Timers from hidden
// messages such as read receipts can be useful to detect
// ephemeral timer support, but timer changes without visible
// received messages may be confusing to the user.
if !*hidden
&& !location_kml_is
&& !is_mdn
&& chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
{
info!(
@@ -972,35 +955,40 @@ async fn add_parts(
warn!(context, "verification problem: {}", err);
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(s);
} else {
// change chat protection only when verification check passes
if let Some(new_status) = new_status {
if chat_id
} else if let Some(new_status) = new_status {
if !chat.is_protected()
&& new_status == ProtectionStatus::Protected
&& chat_id
.update_timestamp(
context,
Param::ProtectionSettingsTimestamp,
sent_timestamp,
)
.await?
{
// Upgrade chat to a protected chat.
// As this gives some guarantees to the user,
// we do not allow downgrades over the wire.
if let Err(e) = chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await
{
if let Err(e) = chat_id.inner_set_protection(context, new_status).await {
chat::add_info_msg(
context,
chat_id,
format!("Cannot set protection: {}", e),
sort_timestamp,
)
.await?;
return Ok(chat_id); // do not return an error as this would result in retrying the message
}
chat::add_info_msg(
context,
chat_id,
format!("Cannot set protection: {}", e),
sort_timestamp,
)
.await?;
return Ok(chat_id); // do not return an error as this would result in retrying the message
}
set_better_msg(
mime_parser,
context
.stock_protection_msg(new_status, from_id as u32)
.await,
);
}
set_better_msg(
mime_parser,
context
.stock_protection_msg(new_status, from_id as u32)
.await,
);
}
}
}
@@ -1059,6 +1047,9 @@ async fn add_parts(
Vec::new()
};
let is_hidden = *hidden;
let mut is_hidden = is_hidden;
let mut ids = Vec::with_capacity(parts.len());
let conn = context.sql.get_conn().await?;
@@ -1073,7 +1064,7 @@ INSERT INTO msgs
from_id, to_id, timestamp, timestamp_sent,
timestamp_rcvd, type, state, msgrmsg,
txt, subject, txt_raw, param,
bytes, mime_headers, mime_in_reply_to,
bytes, hidden, mime_headers, mime_in_reply_to,
mime_references, mime_modified, error, ephemeral_timer,
ephemeral_timestamp, download_state
)
@@ -1084,11 +1075,21 @@ INSERT INTO msgs
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?
?, ?
);
"#,
)?;
let is_location_kml =
location_kml_is && icnt == 1 && (part.msg == "-location-" || part.msg.is_empty());
if is_mdn || is_location_kml {
is_hidden = true;
if incoming {
state = MessageState::InSeen; // Set the state to InSeen so that precheck_imf() adds a markseen job after we moved the message
}
}
let part_is_empty = part.msg.is_empty() && part.param.get(Param::Quote).is_none();
let mime_modified = save_mime_modified && !part_is_empty;
if mime_modified {
@@ -1142,6 +1143,7 @@ INSERT INTO msgs
part.param.to_string()
},
part.bytes as isize,
is_hidden,
if (save_mime_headers || mime_modified) && !trash {
mime_headers.clone()
} else {
@@ -1166,8 +1168,11 @@ INSERT INTO msgs
}
drop(conn);
chat_id.unarchive(context).await?;
if !is_hidden {
chat_id.unarchive(context).await?;
}
*hidden = is_hidden;
created_db_entries.extend(ids.iter().map(|id| (chat_id, *id)));
mime_parser.parts = parts;
@@ -1177,12 +1182,12 @@ INSERT INTO msgs
);
// new outgoing message from another device marks the chat as noticed.
if !incoming && !chat_id.is_special() {
if !incoming && !*hidden && !chat_id.is_special() {
chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?;
}
// check event to send
*create_event_to_send = if chat_id.is_trash() {
*create_event_to_send = if chat_id.is_trash() || *hidden {
None
} else if incoming && state == MessageState::InFresh {
Some(CreateEvent::IncomingMsg)
@@ -1212,57 +1217,64 @@ INSERT INTO msgs
Ok(chat_id)
}
/// Saves attached locations to the database.
///
/// Emits an event if at least one new location was added.
async fn save_locations(
context: &Context,
mime_parser: &MimeMessage,
chat_id: ChatId,
from_id: u32,
msg_id: MsgId,
) -> Result<()> {
insert_msg_id: MsgId,
hidden: bool,
) {
if chat_id.is_special() {
// Do not save locations for trashed messages.
return Ok(());
return;
}
let mut location_id_written = false;
let mut send_event = false;
if let Some(message_kml) = &mime_parser.message_kml {
if let Some(newest_location_id) =
location::save(context, chat_id, from_id, &message_kml.locations, true).await?
if mime_parser.message_kml.is_some() {
let locations = &mime_parser.message_kml.as_ref().unwrap().locations;
let newest_location_id = location::save(context, chat_id, from_id, locations, true)
.await
.unwrap_or_default();
if 0 != newest_location_id
&& !hidden
&& location::set_msg_location_id(context, insert_msg_id, newest_location_id)
.await
.is_ok()
{
location::set_msg_location_id(context, msg_id, newest_location_id).await?;
location_id_written = true;
send_event = true;
}
}
if let Some(location_kml) = &mime_parser.location_kml {
if let Some(addr) = &location_kml.addr {
let contact = Contact::get_by_id(context, from_id).await?;
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
if let Some(newest_location_id) =
location::save(context, chat_id, from_id, &location_kml.locations, false)
.await?
{
location::set_msg_location_id(context, msg_id, newest_location_id).await?;
if mime_parser.location_kml.is_some() {
if let Some(ref addr) = mime_parser.location_kml.as_ref().unwrap().addr {
if let Ok(contact) = Contact::get_by_id(context, from_id).await {
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
let locations = &mime_parser.location_kml.as_ref().unwrap().locations;
let newest_location_id =
location::save(context, chat_id, from_id, locations, false)
.await
.unwrap_or_default();
if newest_location_id != 0 && !hidden && !location_id_written {
if let Err(err) = location::set_msg_location_id(
context,
insert_msg_id,
newest_location_id,
)
.await
{
error!(context, "Failed to set msg_location_id: {:?}", err);
}
}
send_event = true;
}
} else {
warn!(
context,
"Address in location.kml {:?} is not the same as the sender address {:?}.",
addr,
contact.get_addr()
);
}
}
}
if send_event {
context.emit_event(EventType::LocationChanged(Some(from_id)));
}
Ok(())
}
async fn calc_sort_timestamp(
@@ -1296,7 +1308,7 @@ async fn calc_sort_timestamp(
async fn lookup_chat_by_reply(
context: &Context,
mime_parser: &mut MimeMessage,
mime_parser: &&mut MimeMessage,
parent: &Option<Message>,
from_id: u32,
to_ids: &ContactIds,
@@ -1513,17 +1525,6 @@ async fn create_or_lookup_group(
}
set_better_msg(mime_parser, &better_msg);
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
warn!(context, "verification problem: {}", err);
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(&s);
}
ProtectionStatus::Protected
} else {
ProtectionStatus::Unprotected
};
// check if the group does not exist but should be created
let group_explicitly_left = chat::is_group_explicitly_left(context, &grpid)
.await
@@ -1544,6 +1545,18 @@ async fn create_or_lookup_group(
|| X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
{
// group does not exist but should be created
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await
{
warn!(context, "verification problem: {}", err);
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(&s);
}
ProtectionStatus::Protected
} else {
ProtectionStatus::Unprotected
};
if !allow_creation {
info!(context, "creating group forbidden by caller");
return Ok(None);
@@ -1584,16 +1597,6 @@ async fn create_or_lookup_group(
// .add_protection_msg(context, ProtectionStatus::Protected, false, 0)
// .await?;
//}
} else if let Some(chat_id) = chat_id {
if create_protected == ProtectionStatus::Protected {
let chat = Chat::load_from_db(context, chat_id).await?;
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await?;
recreate_member_list = true;
}
}
}
// again, check chat_id
@@ -2065,7 +2068,7 @@ async fn check_verified_properties(
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
// mark gossiped keys (if any) as verified
if mimeparser.gossiped_addr.contains(&to_addr) {
if mimeparser.gossipped_addr.contains(&to_addr) {
if let Some(mut peerstate) = peerstate {
// if we're here, we know the gossip key is verified:
// - use the gossip-key as verified-key if there is no verified-key

View File

@@ -835,8 +835,8 @@ mod tests {
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
}
use crate::chat;
use crate::chatlist::Chatlist;
use crate::{chat, test_utils};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use proptest::prelude::*;
@@ -844,18 +844,22 @@ mod tests {
#[test]
fn test_dc_truncate(
buf: String,
approx_chars in 0..100usize
approx_chars in 0..10000usize
) {
let res = dc_truncate(&buf, approx_chars);
let el_len = 5;
let l = res.chars().count();
assert!(
l <= approx_chars + el_len,
"buf: '{}' - res: '{}' - len {}, approx {}",
&buf, &res, res.len(), approx_chars
);
if approx_chars > 0 {
assert!(
l <= approx_chars + el_len,
"buf: '{}' - res: '{}' - len {}, approx {}",
&buf, &res, res.len(), approx_chars
);
} else {
assert_eq!(&res, &buf);
}
if buf.chars().count() > approx_chars + el_len {
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
let l = res.len();
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
}
@@ -982,7 +986,8 @@ mod tests {
#[test]
fn test_get_filemeta() {
let (w, h) = dc_get_filemeta(test_utils::AVATAR_900x900_BYTES).unwrap();
let data = include_bytes!("../test-data/image/avatar900x900.png");
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 900);
assert_eq!(h, 900);

View File

@@ -323,7 +323,4 @@ pub enum EventType {
/// dc_get_connectivity_html() for details.
#[strum(props(id = "2100"))]
ConnectivityChanged,
#[strum(props(id = "2110"))]
SelfavatarChanged,
}

View File

@@ -433,13 +433,6 @@ impl Job {
}
// now also delete the generated file
dc_delete_file(context, filename).await;
// finally, create another send-job if there are items to be synced.
// triggering sync-job after msg-send-job guarantees, the recipient has grpid etc.
// once the sync message arrives.
// if there are no items to sync, this function returns fast.
context.send_sync_msg().await?;
Ok(())
}
})
@@ -991,7 +984,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
}
if rendered_msg.is_gossiped {
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
chat::set_gossiped_timestamp(context, msg.chat_id, time()).await?;
}
if 0 != rendered_msg.last_added_location_id {
@@ -1008,12 +1001,6 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
}
}
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
if let Err(err) = context.delete_sync_ids(sync_ids).await {
error!(context, "Failed to delete sync ids: {:?}", err);
}
}
if attach_selfavatar {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
error!(context, "Failed to set selfavatar timestamp: {:?}", err);

View File

@@ -82,7 +82,6 @@ pub mod securejoin;
mod simplify;
mod smtp;
pub mod stock_str;
mod sync;
mod token;
mod update_helper;
#[macro_use]

View File

@@ -552,20 +552,17 @@ pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id:
Ok(())
}
/// Saves given locations to the database.
///
/// Returns the database row ID of the location with the highest timestamp.
pub(crate) async fn save(
pub async fn save(
context: &Context,
chat_id: ChatId,
contact_id: u32,
locations: &[Location],
independent: bool,
) -> Result<Option<u32>> {
) -> Result<u32> {
ensure!(!chat_id.is_special(), "Invalid chat id");
let mut newest_timestamp = 0;
let mut newest_location_id = None;
let mut newest_location_id = 0;
let stmt_insert = "INSERT INTO locations\
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
@@ -603,12 +600,12 @@ pub(crate) async fn save(
drop(stmt_test);
drop(stmt_insert);
newest_timestamp = timestamp;
newest_location_id = Some(u32::try_from(conn.last_insert_rowid())?);
newest_location_id = conn.last_insert_rowid();
}
}
}
Ok(newest_location_id)
Ok(u32::try_from(newest_location_id)?)
}
pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> job::Status {

View File

@@ -1,7 +1,6 @@
//! # Logging.
use crate::context::Context;
use async_std::task::block_on;
#[macro_export]
macro_rules! info {
@@ -40,28 +39,10 @@ macro_rules! error {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
$ctx.set_last_error(&formatted);
$ctx.emit_event($crate::EventType::Error(formatted));
}};
}
impl Context {
/// Set last error string.
/// Implemented as blocking as used from macros in different, not always async blocks.
pub fn set_last_error(&self, error: &str) {
block_on(async move {
let mut last_error = self.last_error.write().await;
*last_error = error.to_string();
});
}
/// Get last error string.
pub async fn get_last_error(&self) -> String {
let last_error = &*self.last_error.read().await;
last_error.clone()
}
}
pub trait LogExt<T, E>
where
Self: std::marker::Sized,
@@ -153,31 +134,3 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
self
}
}
#[cfg(test)]
mod tests {
use crate::test_utils::TestContext;
use anyhow::Result;
#[async_std::test]
async fn test_get_last_error() -> Result<()> {
let t = TestContext::new().await;
assert_eq!(t.get_last_error().await, "");
error!(t, "foo-error");
assert_eq!(t.get_last_error().await, "foo-error");
warn!(t, "foo-warning");
assert_eq!(t.get_last_error().await, "foo-error");
info!(t, "foo-info");
assert_eq!(t.get_last_error().await, "foo-error");
error!(t, "bar-error");
error!(t, "baz-error");
assert_eq!(t.get_last_error().await, "baz-error");
Ok(())
}
}

View File

@@ -11,10 +11,8 @@ use anyhow::Result;
use async_std::io;
use async_std::net::TcpStream;
use async_native_tls::Certificate;
pub use async_smtp::ServerAddress;
use fast_socks5::client::Socks5Stream;
use once_cell::sync::Lazy;
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
#[repr(u32)]
@@ -370,18 +368,8 @@ fn get_readable_flags(flags: i32) -> String {
res
}
// this certificate is missing on older android devices (eg. lg with android6 from 2017)
// certificate downloaded from https://letsencrypt.org/certificates/
static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
Certificate::from_der(include_bytes!(
"../assets/root-certificates/letsencrypt/isrgrootx1.der"
))
.unwrap()
});
pub fn dc_build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
let tls_builder =
async_native_tls::TlsConnector::new().add_root_certificate(LETSENCRYPT_ROOT.clone());
let tls_builder = async_native_tls::TlsConnector::new();
if strict_tls {
tls_builder
@@ -442,13 +430,4 @@ mod tests {
assert_eq!(param, loaded);
Ok(())
}
#[async_std::test]
async fn test_build_tls() -> Result<()> {
// we are using some additional root certificates.
// make sure, they do not break construction of TlsConnector
let _ = dc_build_tls(true);
let _ = dc_build_tls(false);
Ok(())
}
}

View File

@@ -68,13 +68,6 @@ pub struct MimeFactory<'a> {
references: String,
req_mdn: bool,
last_added_location_id: u32,
/// If the created mime-structure contains sync-items,
/// the IDs of these items are listed here.
/// The IDs are returned via `RenderedEmail`
/// and must be deleted if the message is actually queued for sending.
sync_ids_to_delete: Option<String>,
attach_selfavatar: bool,
}
@@ -87,12 +80,6 @@ pub struct RenderedEmail {
pub is_gossiped: bool,
pub last_added_location_id: u32,
/// A comma-separated string of sync-IDs that are used by the rendered email
/// and must be deleted once the message is actually queued for sending
/// (deletion must be done by `delete_sync_ids()`).
/// If the rendered email is not queued for sending, the IDs must not be deleted.
pub sync_ids_to_delete: Option<String>,
/// Message ID (Message in the sense of Email)
pub rfc724_mid: String,
pub subject: String,
@@ -218,7 +205,6 @@ impl<'a> MimeFactory<'a> {
references,
req_mdn,
last_added_location_id: 0,
sync_ids_to_delete: None,
attach_selfavatar,
};
Ok(factory)
@@ -263,7 +249,6 @@ impl<'a> MimeFactory<'a> {
references: String::default(),
req_mdn: false,
last_added_location_id: 0,
sync_ids_to_delete: None,
attach_selfavatar: false,
};
@@ -361,7 +346,7 @@ impl<'a> MimeFactory<'a> {
match &self.loaded {
Loaded::Message { chat } => {
// beside key- and member-changes, force re-gossip every 48 hours
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
let gossiped_timestamp = chat.get_gossiped_timestamp(context).await?;
if time() > gossiped_timestamp + (2 * 24 * 60 * 60) {
Ok(true)
} else {
@@ -618,20 +603,12 @@ impl<'a> MimeFactory<'a> {
main_part
} else {
// Multiple parts, render as multipart.
let part_holder = if self.msg.param.get_cmd() == SystemMessage::MultiDeviceSync {
PartBuilder::new().header((
"Content-Type".to_string(),
"multipart/report; report-type=multi-device-sync".to_string(),
))
} else {
PartBuilder::new().message_type(MimeMultipartType::Mixed)
};
parts
.into_iter()
.fold(part_holder.child(main_part.build()), |message, part| {
message.child(part.build())
})
parts.into_iter().fold(
PartBuilder::new()
.message_type(MimeMultipartType::Mixed)
.child(main_part.build()),
|message, part| message.child(part.build()),
)
};
let outer_message = if is_encrypted {
@@ -752,7 +729,6 @@ impl<'a> MimeFactory<'a> {
is_encrypted,
is_gossiped,
last_added_location_id,
sync_ids_to_delete: self.sync_ids_to_delete,
rfc724_mid,
subject: subject_str,
})
@@ -897,7 +873,7 @@ impl<'a> MimeFactory<'a> {
"ephemeral-timer-changed".to_string(),
));
}
SystemMessage::LocationOnly | SystemMessage::MultiDeviceSync => {
SystemMessage::LocationOnly => {
// This should prevent automatic replies,
// such as non-delivery reports.
//
@@ -1127,15 +1103,6 @@ impl<'a> MimeFactory<'a> {
}
}
// we do not piggyback sync-files to other self-sent-messages
// to not risk files becoming too larger and being skipped by download-on-demand.
if command == SystemMessage::MultiDeviceSync && self.is_e2ee_guaranteed() {
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
let ids = self.msg.param.get(Param::Arg2).unwrap_or_default();
parts.push(context.build_sync_part(json.to_string()).await);
self.sync_ids_to_delete = Some(ids.to_string());
}
if self.attach_selfavatar {
match context.get_config(Config::Selfavatar).await? {
Some(path) => match build_selfavatar_file(context, &path) {

View File

@@ -28,7 +28,6 @@ use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::simplify::simplify;
use crate::stock_str;
use crate::sync::SyncItems;
/// A parsed MIME message.
///
@@ -57,14 +56,11 @@ pub struct MimeMessage {
/// this set is empty.
pub signatures: HashSet<Fingerprint>,
/// The set of mail recipient addresses for which gossip headers were applied, regardless of
/// whether they modified any peerstates.
pub gossiped_addr: HashSet<String>,
pub gossipped_addr: HashSet<String>,
pub is_forwarded: bool,
pub is_system_message: SystemMessage,
pub location_kml: Option<location::Kml>,
pub message_kml: Option<location::Kml>,
pub(crate) sync_items: Option<SyncItems>,
pub(crate) user_avatar: Option<AvatarAction>,
pub(crate) group_avatar: Option<AvatarAction>,
pub(crate) mdn_reports: Vec<Report>,
@@ -128,10 +124,6 @@ pub enum SystemMessage {
// Chat protection state changed
ChatProtectionEnabled = 11,
ChatProtectionDisabled = 12,
/// Self-sent-message that contains only json used for multi-device-sync;
/// if possible, we attach that to other messages as for locations.
MultiDeviceSync = 20,
}
impl Default for SystemMessage {
@@ -200,7 +192,7 @@ impl MimeMessage {
// Memory location for a possible decrypted message.
let mut mail_raw = Vec::new();
let mut gossiped_addr = Default::default();
let mut gossipped_addr = Default::default();
let (mail, signatures, warn_empty_signature) =
match e2ee::try_decrypt(context, &mail, message_time).await {
@@ -223,7 +215,7 @@ impl MimeMessage {
if !signatures.is_empty() {
let gossip_headers =
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossiped_addr = update_gossip_peerstates(
gossipped_addr = update_gossip_peerstates(
context,
message_time,
&mail,
@@ -281,13 +273,12 @@ impl MimeMessage {
// only non-empty if it was a valid autocrypt message
signatures,
gossiped_addr,
gossipped_addr,
is_forwarded: false,
mdn_reports: Vec::new(),
is_system_message: SystemMessage::Unknown,
location_kml: None,
message_kml: None,
sync_items: None,
user_avatar: None,
group_avatar: None,
failure_report: None,
@@ -822,12 +813,6 @@ impl MimeMessage {
}
}
}
Some("multi-device-sync") => {
if let Some(second) = mail.subparts.get(1) {
self.add_single_part_if_known(context, second, is_related)
.await?;
}
}
Some(_) => {
if let Some(first) = mail.subparts.get(0) {
any_part_added = self
@@ -1014,20 +999,7 @@ impl MimeMessage {
}
return;
}
} else if filename == "multi-device-sync.json" {
let serialized = String::from_utf8_lossy(decoded_data)
.parse()
.unwrap_or_default();
self.sync_items = context
.parse_sync_items(serialized)
.await
.map_err(|err| {
warn!(context, "failed to parse sync data: {}", err);
})
.ok();
return;
}
/* we have a regular file attachment,
write decoded data to new blob object */
@@ -1382,9 +1354,6 @@ impl MimeMessage {
}
}
/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates.
///
/// Returns the set of mail recipient addresses for which valid gossip headers were found.
async fn update_gossip_peerstates(
context: &Context,
message_time: i64,
@@ -1392,46 +1361,42 @@ async fn update_gossip_peerstates(
gossip_headers: Vec<String>,
) -> Result<HashSet<String>> {
// XXX split the parsing from the modification part
let mut gossiped_addr: HashSet<String> = Default::default();
let mut gossipped_addr: HashSet<String> = Default::default();
for value in &gossip_headers {
let header = match value.parse::<Aheader>() {
Ok(header) => header,
Err(err) => {
warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
continue;
let gossip_header = value.parse::<Aheader>();
if let Ok(ref header) = gossip_header {
if get_recipients(&mail.headers)
.iter()
.any(|info| info.addr == header.addr.to_lowercase())
{
let mut peerstate = Peerstate::from_addr(context, &header.addr).await?;
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_gossip(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_gossip(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
if let Some(peerstate) = peerstate {
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
}
gossipped_addr.insert(header.addr.clone());
} else {
warn!(
context,
"Ignoring gossipped \"{}\" as the address is not in To/Cc list.", &header.addr,
);
}
};
if !get_recipients(&mail.headers)
.iter()
.any(|info| info.addr == header.addr.to_lowercase())
{
warn!(
context,
"Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
);
continue;
}
let peerstate;
if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? {
p.apply_gossip(&header, message_time);
p.save_to_db(&context.sql, false).await?;
peerstate = p;
} else {
let p = Peerstate::from_gossip(&header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = p;
};
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
gossiped_addr.insert(header.addr.clone());
}
Ok(gossiped_addr)
Ok(gossipped_addr)
}
#[derive(Debug)]

View File

@@ -4,7 +4,7 @@ mod data;
use crate::config::Config;
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS, PROVIDER_UPDATED};
use async_std_resolver::resolver_from_system_conf;
use async_std_resolver::{config, resolver};
use chrono::{NaiveDateTime, NaiveTime};
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
@@ -118,7 +118,12 @@ pub fn get_provider_by_domain(domain: &str) -> Option<&'static Provider> {
///
/// For security reasons, only Gmail can be configured this way.
pub async fn get_provider_by_mx(domain: &str) -> Option<&'static Provider> {
if let Ok(resolver) = resolver_from_system_conf().await {
if let Ok(resolver) = resolver(
config::ResolverConfig::default(),
config::ResolverOpts::default(),
)
.await
{
let mut fqdn: String = domain.to_string();
if !fqdn.ends_with('.') {
fqdn.push('.');

View File

@@ -391,10 +391,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
} => {
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
token::delete(context, token::Namespace::Auth, &authcode).await?;
context
.sync_qr_code_token_deletion(invitenumber, authcode)
.await?;
context.send_sync_msg().await?;
}
Qr::WithdrawVerifyGroup {
invitenumber,
@@ -403,10 +399,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
} => {
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
token::delete(context, token::Namespace::Auth, &authcode).await?;
context
.sync_qr_code_token_deletion(invitenumber, authcode)
.await?;
context.send_sync_msg().await?;
}
Qr::ReviveVerifyContact {
invitenumber,
@@ -415,8 +407,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
} => {
token::save(context, token::Namespace::InviteNumber, None, &invitenumber).await?;
token::save(context, token::Namespace::Auth, None, &authcode).await?;
context.sync_qr_code_tokens(None).await?;
context.send_sync_msg().await?;
}
Qr::ReviveVerifyGroup {
invitenumber,
@@ -435,8 +425,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
)
.await?;
token::save(context, token::Namespace::Auth, chat_id, &authcode).await?;
context.sync_qr_code_tokens(chat_id).await?;
context.send_sync_msg().await?;
}
_ => bail!("qr code {:?} does not contain config", qr),
}

View File

@@ -1,15 +1,17 @@
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol).
use std::convert::TryFrom;
use std::time::{Duration, Instant};
use anyhow::{bail, Context as _, Error, Result};
use anyhow::{anyhow, bail, Context as _, Error, Result};
use async_std::channel::Receiver;
use async_std::sync::Mutex;
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, is_contact_in_chat, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
use crate::contact::{Contact, Origin, VerifiedStatus};
use crate::context::Context;
use crate::dc_tools::time;
@@ -28,7 +30,6 @@ use crate::token;
mod bobstate;
mod qrinvite;
use crate::token::Namespace;
use bobstate::{BobHandshakeStage, BobState, BobStateHandle};
use qrinvite::QrInvite;
@@ -79,8 +80,8 @@ enum StartedProtocolVariant {
SetupContact,
/// The secure-join protocol, to join a group.
SecureJoin {
ongoing_receiver: Receiver<()>,
group_id: String,
group_name: String,
},
}
@@ -108,14 +109,16 @@ impl Bob {
*guard = None;
}
let variant = match invite {
QrInvite::Group {
ref grpid,
ref name,
..
} => StartedProtocolVariant::SecureJoin {
group_id: grpid.clone(),
group_name: name.clone(),
},
QrInvite::Group { ref grpid, .. } => {
let receiver = context
.alloc_ongoing()
.await
.map_err(|_| JoinError::OngoingRunning)?;
StartedProtocolVariant::SecureJoin {
ongoing_receiver: receiver,
group_id: grpid.clone(),
}
}
_ => StartedProtocolVariant::SetupContact,
};
match BobState::start_protocol(context, invite).await {
@@ -168,11 +171,8 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
// invitenumber will be used to allow starting the handshake,
// auth will be used to verify the fingerprint
let sync_token = token::lookup(context, Namespace::InviteNumber, group)
.await?
.is_none();
let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, group).await;
let auth = token::lookup_or_new(context, Namespace::Auth, group).await;
let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group).await;
let auth = token::lookup_or_new(context, token::Namespace::Auth, group).await;
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
Ok(Some(addr)) => addr,
Ok(None) => {
@@ -208,9 +208,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
let chat = Chat::load_from_db(context, group).await?;
let group_name = chat.get_name();
let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
if sync_token {
context.sync_qr_code_tokens(Some(chat.id)).await?;
}
format!(
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
fingerprint.hex(),
@@ -222,9 +220,6 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
)
} else {
// parameters used: a=n=i=s=
if sync_token {
context.sync_qr_code_tokens(None).await?;
}
format!(
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
fingerprint.hex(),
@@ -276,7 +271,9 @@ pub enum JoinError {
/// This is the start of the process for the joiner. See the module and ffi documentation
/// for more details.
///
/// The function returns immediately and the handshake will run in background.
/// When joining a group this will start an "ongoing" process and will block until the
/// process is completed, the [`ChatId`] for the new group is not known any sooner. When
/// verifying a contact this returns immediately.
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
securejoin(context, qr).await.map_err(|err| {
warn!(context, "Fatal joiner error: {:#}", err);
@@ -307,34 +304,39 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
Ok(chat_id)
}
StartedProtocolVariant::SecureJoin {
ongoing_receiver,
group_id,
group_name,
} => {
// for a group-join, also create the chat soon and let the verification run in background.
// however, the group will become usable only when the protocol has finished.
let contact_id = invite.contact_id();
let chat_id = if let Some((chat_id, _protected, _blocked)) =
chat::get_chat_id_by_grpid(context, &group_id).await?
{
chat_id.unblock(context).await?;
chat_id
} else {
ChatId::create_multiuser_record(
context,
Chattype::Group,
group_id,
group_name,
Blocked::Not,
ProtectionStatus::Unprotected, // protection is added later as needed
)
.await?
// for a group-join, wait until the protocol is finished and the group is created
ongoing_receiver
.recv()
.await
.map_err(|_| JoinError::OngoingSenderDropped)?;
// handle_securejoin_handshake() calls Context::stop_ongoing before the group
// chat is created (it is created after handle_securejoin_handshake() returns by
// dc_receive_imf()). As a hack we just wait a bit for it to appear.
// If the protocol is aborted by Bob, this timeout will also happen.
let start = Instant::now();
let chatid = loop {
{
match chat::get_chat_id_by_grpid(context, &group_id).await? {
Some((chatid, _is_protected, _blocked)) => break chatid,
None => {
if start.elapsed() > Duration::from_secs(7) {
context.free_ongoing().await;
return Err(JoinError::Other(anyhow!(
"Ongoing sender dropped (this is a bug)"
)));
}
}
}
}
async_std::task::sleep(Duration::from_millis(50)).await;
};
if !is_contact_in_chat(context, chat_id, contact_id).await? {
chat::add_to_chat_contacts_table(context, chat_id, contact_id).await?;
}
let msg = stock_str::secure_join_started(context, contact_id).await;
chat::add_info_msg(context, chat_id, msg, time()).await?;
Ok(chat_id)
context.free_ongoing().await;
Ok(chatid)
}
}
}
@@ -347,13 +349,13 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
#[error("Failed sending handshake message")]
pub struct SendMsgError(#[from] anyhow::Error);
/// Send handshake message from Alice's device;
/// Bob's handshake messages are sent in `BobState::send_handshake_message()`.
async fn send_alice_handshake_msg(
async fn send_handshake_msg(
context: &Context,
contact_id: u32,
contact_chat_id: ChatId,
step: &str,
param2: &str,
fingerprint: Option<Fingerprint>,
grpid: &str,
) -> Result<(), SendMsgError> {
let mut msg = Message {
viewtype: Viewtype::Text,
@@ -362,55 +364,67 @@ async fn send_alice_handshake_msg(
..Default::default()
};
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
msg.param.set(Param::Arg, step);
if step.is_empty() {
msg.param.remove(Param::Arg);
} else {
msg.param.set(Param::Arg, step);
}
if !param2.is_empty() {
msg.param.set(Param::Arg2, param2);
}
if let Some(fp) = fingerprint {
msg.param.set(Param::Arg3, fp.hex());
}
msg.param.set_int(Param::GuaranteeE2ee, 1);
chat::send_msg(
context,
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
.await?
.id,
&mut msg,
)
.await?;
if !grpid.is_empty() {
msg.param.set(Param::Arg4, grpid);
}
if step == "vg-request" || step == "vc-request" {
msg.param.set_int(Param::ForcePlaintext, 1);
} else {
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
chat::send_msg(context, contact_chat_id, &mut msg).await?;
Ok(())
}
/// Get an unblocked chat that can be used for info messages.
async fn info_chat_id(context: &Context, contact_id: u32) -> Result<ChatId> {
let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?;
Ok(chat_id_blocked.id)
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> Result<u32, Error> {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
Ok(contact_id)
} else {
Ok(0)
}
}
async fn fingerprint_equals_sender(
context: &Context,
fingerprint: &Fingerprint,
contact_id: u32,
contact_chat_id: ChatId,
) -> Result<bool, Error> {
let contact = Contact::load_from_db(context, contact_id).await?;
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
Ok(peerstate) => peerstate,
Err(err) => {
warn!(
context,
"Failed to sender peerstate for {}: {}",
contact.get_addr(),
err
);
return Ok(false);
}
};
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
Ok(peerstate) => peerstate,
Err(err) => {
warn!(
context,
"Failed to sender peerstate for {}: {}",
contact.get_addr(),
err
);
return Ok(false);
}
};
if let Some(peerstate) = peerstate {
if peerstate.public_key_fingerprint.is_some()
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
{
return Ok(true);
if let Some(peerstate) = peerstate {
if peerstate.public_key_fingerprint.is_some()
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
{
return Ok(true);
}
}
}
}
Ok(false)
}
@@ -471,6 +485,21 @@ pub(crate) async fn handle_securejoin_handshake(
">>>>>>>>>>>>>>>>>>>>>>>>> secure-join message \'{}\' received", step,
);
let contact_chat_id = {
let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
.await
.with_context(|| {
format!(
"Failed to look up or create chat for contact {}",
contact_id
)
})?;
if chat.blocked != Blocked::Not {
chat.id.unblock(context).await?;
}
chat.id
};
let join_vg = step.starts_with("vg-");
match step.as_str() {
@@ -499,18 +528,14 @@ pub(crate) async fn handle_securejoin_handshake(
inviter_progress!(context, contact_id, 300);
// for setup-contact, make Alice's one-to-one chat with Bob visible
// (secure-join-information are shown in the group chat)
if !join_vg {
ChatId::create_for_contact(context, contact_id).await?;
}
// Alice -> Bob
send_alice_handshake_msg(
send_handshake_msg(
context,
contact_id,
contact_chat_id,
&format!("{}-auth-required", &step[..2]),
"",
None,
"",
)
.await?;
Ok(HandshakeMessage::Done)
@@ -523,19 +548,11 @@ pub(crate) async fn handle_securejoin_handshake(
match context.bob.state(context).await {
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(
context,
contact_id,
bobstate.chat_id(context).await?,
why,
)
.await?;
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
.await?;
Ok(HandshakeMessage::Done)
}
Some(_stage) => {
let msg = stock_str::secure_join_replies(context, contact_id).await;
chat::add_info_msg(context, bobstate.chat_id(context).await?, msg, time())
.await?;
joiner_progress!(context, bobstate.invite().contact_id(), 400);
Ok(HandshakeMessage::Done)
}
@@ -558,8 +575,7 @@ pub(crate) async fn handle_securejoin_handshake(
None => {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
contact_chat_id,
"Fingerprint not provided.",
)
.await?;
@@ -569,18 +585,16 @@ pub(crate) async fn handle_securejoin_handshake(
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
contact_chat_id,
"Auth not encrypted.",
)
.await?;
return Ok(HandshakeMessage::Ignore);
}
if !fingerprint_equals_sender(context, &fingerprint, contact_id).await? {
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await? {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
)
.await?;
@@ -593,8 +607,7 @@ pub(crate) async fn handle_securejoin_handshake(
None => {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
contact_chat_id,
"Auth not provided.",
)
.await?;
@@ -602,20 +615,14 @@ pub(crate) async fn handle_securejoin_handshake(
}
};
if !token::exists(context, token::Namespace::Auth, auth_0).await {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
"Auth invalid.",
)
.await?;
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.")
.await?;
return Ok(HandshakeMessage::Ignore);
}
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
)
.await?;
@@ -623,6 +630,7 @@ pub(crate) async fn handle_securejoin_handshake(
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
info!(context, "Auth verified.",);
secure_connection_established(context, contact_chat_id).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600);
if join_vg {
@@ -638,7 +646,6 @@ pub(crate) async fn handle_securejoin_handshake(
};
match chat::get_chat_id_by_grpid(context, field_grpid).await? {
Some((group_chat_id, _, _)) => {
secure_connection_established(context, contact_id, group_chat_id).await?;
if let Err(err) =
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
.await
@@ -650,17 +657,13 @@ pub(crate) async fn handle_securejoin_handshake(
}
} else {
// Alice -> Bob
secure_connection_established(
send_handshake_msg(
context,
contact_id,
info_chat_id(context, contact_id).await?,
)
.await?;
send_alice_handshake_msg(
context,
contact_id,
contact_chat_id,
"vc-contact-confirm",
"",
Some(fingerprint),
"",
)
.await?;
@@ -682,23 +685,13 @@ pub(crate) async fn handle_securejoin_handshake(
match context.bob.state(context).await {
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(
context,
contact_id,
bobstate.chat_id(context).await?,
why,
)
.await?;
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
.await?;
Ok(HandshakeMessage::Done)
}
Some(BobHandshakeStage::Completed) => {
// Can only be BobHandshakeStage::Completed
secure_connection_established(
context,
contact_id,
bobstate.chat_id(context).await?,
)
.await?;
secure_connection_established(context, bobstate.chat_id()).await?;
Ok(retval)
}
Some(_) => {
@@ -782,6 +775,21 @@ pub(crate) async fn observe_securejoin_on_other_device(
.context("Not a Secure-Join message")?;
info!(context, "observing secure-join message \'{}\'", step);
let contact_chat_id = {
let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
.await
.with_context(|| {
format!(
"Failed to look up or create chat for contact {}",
contact_id
)
})?;
if chat.blocked != Blocked::Not {
chat.id.unblock(context).await?;
}
chat.id
};
match step.as_str() {
"vg-member-added"
| "vc-contact-confirm"
@@ -794,8 +802,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
) {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
contact_chat_id,
"Message not encrypted correctly.",
)
.await?;
@@ -807,8 +814,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
None => {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
contact_chat_id,
"Fingerprint not provided, please update Delta Chat on all your devices.",
)
.await?;
@@ -818,8 +824,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
contact_chat_id,
format!("Fingerprint mismatch on observing {}.", step).as_ref(),
)
.await?;
@@ -837,22 +842,30 @@ pub(crate) async fn observe_securejoin_on_other_device(
async fn secure_connection_established(
context: &Context,
contact_id: u32,
chat_id: ChatId,
contact_chat_id: ChatId,
) -> Result<(), Error> {
let contact = Contact::get_by_id(context, contact_id).await?;
let msg = stock_str::contact_verified(context, contact.get_name_n_addr()).await;
chat::add_info_msg(context, chat_id, msg, time()).await?;
context.emit_event(EventType::ChatModified(chat_id));
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
let contact = Contact::get_by_id(context, contact_id).await;
let addr = if let Ok(ref contact) = contact {
contact.get_addr()
} else {
"?"
};
let msg = stock_str::contact_verified(context, addr).await;
chat::add_info_msg(context, contact_chat_id, msg, time()).await?;
context.emit_event(EventType::ChatModified(contact_chat_id));
info!(context, "StockMessage::ContactVerified posted to 1:1 chat");
Ok(())
}
async fn could_not_establish_secure_connection(
context: &Context,
contact_id: u32,
chat_id: ChatId,
contact_chat_id: ChatId,
details: &str,
) -> Result<(), Error> {
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
let contact = Contact::get_by_id(context, contact_id).await;
let msg = stock_str::contact_not_verified(
context,
@@ -863,11 +876,13 @@ async fn could_not_establish_secure_connection(
},
)
.await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
chat::add_info_msg(context, contact_chat_id, &msg, time()).await?;
error!(
context,
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
);
Ok(())
}
@@ -931,19 +946,15 @@ mod tests {
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::events::Event;
use crate::peerstate::Peerstate;
use crate::test_utils::TestContext;
use std::time::Duration;
#[async_std::test]
async fn test_setup_contact() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
// Setup JoinerProgress sinks.
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
@@ -962,7 +973,6 @@ mod tests {
// Step 2: Bob scans QR-code, sends vc-request
dc_join_securejoin(&bob.ctx, &qr).await?;
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
let sent = bob.pop_sent_msg().await;
assert!(!bob.ctx.has_ongoing().await);
@@ -974,7 +984,6 @@ mod tests {
// Step 3: Alice receives vc-request, sends vc-auth-required
alice.recv_msg(&sent).await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
@@ -1051,11 +1060,6 @@ mod tests {
VerifiedStatus::BidirectVerified
);
// exactly one one-to-one chat should be visible for both now
// (check this before calling alice.create_chat() explicitly below)
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
// Check Alice got the verified message in her 1:1 chat.
{
let chat = alice.create_chat(&bob).await;
@@ -1317,8 +1321,6 @@ mod tests {
async fn test_secure_join() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
// Setup JoinerProgress sinks.
let (joiner_progress_tx, joiner_progress_rx) = async_std::channel::bounded(100);
@@ -1340,11 +1342,15 @@ mod tests {
.await
.unwrap();
// Step 2: Bob scans QR-code, sends vg-request
let bob_chatid = dc_join_securejoin(&bob.ctx, &qr).await?;
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
// Step 2: Bob scans QR-code, sends vg-request; blocks on ongoing process
let joiner = {
let qr = qr.clone();
let ctx = bob.ctx.clone();
async_std::task::spawn(async move { dc_join_securejoin(&ctx, &qr).await.unwrap() })
};
let sent = bob.pop_sent_msg().await;
assert!(bob.ctx.has_ongoing().await);
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
@@ -1458,24 +1464,10 @@ mod tests {
"vg-member-added-received"
);
let bob_chatid = joiner.await;
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
assert!(bob_chat.is_protected());
assert!(bob_chat.typ == Chattype::Group);
assert!(!bob.ctx.has_ongoing().await);
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
// The one-to-one chats are used internally for the hidden handshake messages,
// however, should not be visible in the UIs.
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
let bobs_chat_with_alice = bob.create_chat(&alice).await;
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
alice.recv_msg(&sent).await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
Ok(())
}
}

View File

@@ -8,11 +8,11 @@
//! protocol. Afterwards it must be stored in a mutex and the [`BobStateHandle`] should be
//! used to work with the state.
use anyhow::{bail, Error, Result};
use anyhow::{Error, Result};
use async_std::sync::MutexGuard;
use crate::chat::{self, ChatId};
use crate::constants::{Blocked, Viewtype};
use crate::constants::Viewtype;
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::events::EventType;
@@ -67,18 +67,9 @@ impl<'a> BobStateHandle<'a> {
})
}
/// Returns the [`ChatId`] of the group chat to join or the 1:1 chat with Alice.
pub async fn chat_id(&self, context: &Context) -> Result<ChatId> {
match self.bobstate.invite {
QrInvite::Group { ref grpid, .. } => {
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, &grpid).await? {
Ok(chat_id)
} else {
bail!("chat not found")
}
}
QrInvite::Contact { .. } => Ok(self.bobstate.chat_id),
}
/// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice).
pub fn chat_id(&self) -> ChatId {
self.bobstate.chat_id
}
/// Returns a reference to the [`QrInvite`] of the joiner process.
@@ -194,11 +185,10 @@ impl BobState {
context: &Context,
invite: QrInvite,
) -> Result<(Self, BobHandshakeStage), JoinError> {
let chat_id =
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), Blocked::Yes)
.await
.map_err(JoinError::UnknownContact)?;
if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await? {
let chat_id = ChatId::create_for_contact(context, invite.contact_id())
.await
.map_err(JoinError::UnknownContact)?;
if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await? {
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
info!(context, "Taking securejoin protocol shortcut");
let state = Self {
@@ -307,9 +297,7 @@ impl BobState {
self.next = SecureJoinStep::Terminated;
return Ok(Some(BobHandshakeStage::Terminated(reason)));
}
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.invite.contact_id())
.await?
{
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await? {
self.next = SecureJoinStep::Terminated;
return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch")));
}

View File

@@ -487,16 +487,6 @@ paramsv![]
)
.await?;
}
if dbversion < 80 {
info!(context, "[migration] v80");
sql.execute_migration(
r#"CREATE TABLE multi_device_sync (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item TEXT DEFAULT '');"#,
80,
)
.await?;
}
Ok((
recalc_fingerprints,

View File

@@ -276,15 +276,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Download maximum available until %1$s"))]
DownloadAvailability = 100,
#[strum(props(fallback = "Multi Device Synchronization"))]
SyncMsgSubject = 101,
#[strum(props(
fallback = "This message is used to synchronize data between your devices.\n\n\
👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
))]
SyncMsgBody = 102,
#[strum(props(fallback = "Incoming Messages"))]
IncomingMessages = 103,
@@ -326,13 +317,6 @@ pub enum StockMessage {
#[strum(props(fallback = "%1$s of %2$s used"))]
PartOfTotallUsed = 116,
#[strum(props(fallback = "%1$s invited you to join this group.\n\n\
Waiting for the device of %2$s to reply…"))]
SecureJoinStarted = 117,
#[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
SecureJoinReplies = 118,
}
impl StockMessage {
@@ -598,32 +582,6 @@ pub(crate) async fn e2e_preferred(context: &Context) -> String {
translated(context, StockMessage::E2ePreferred).await
}
/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`.
pub(crate) async fn secure_join_started(context: &Context, inviter_contact_id: u32) -> String {
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
translated(context, StockMessage::SecureJoinStarted)
.await
.replace1(contact.get_name_n_addr())
.replace2(contact.get_display_name())
} else {
format!(
"secure_join_started: unknown contact {}",
inviter_contact_id
)
}
}
/// Stock string: `%1$s replied, waiting for being added to the group…`.
pub(crate) async fn secure_join_replies(context: &Context, contact_id: u32) -> String {
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
translated(context, StockMessage::SecureJoinReplies)
.await
.replace1(contact.get_display_name())
} else {
format!("secure_join_replies: unknown contact {}", contact_id)
}
}
/// Stock string: `%1$s verified.`.
pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef<str>) -> String {
translated(context, StockMessage::ContactVerified)
@@ -666,16 +624,6 @@ pub(crate) async fn ac_setup_msg_body(context: &Context) -> String {
translated(context, StockMessage::AcSetupMsgBody).await
}
/// Stock string: `Multi Device Synchronization`.
pub(crate) async fn sync_msg_subject(context: &Context) -> String {
translated(context, StockMessage::SyncMsgSubject).await
}
/// Stock string: `This message is used to synchronize data betweeen your devices.`.
pub(crate) async fn sync_msg_body(context: &Context) -> String {
translated(context, StockMessage::SyncMsgBody).await
}
/// Stock string: `Cannot login as \"%1$s\". Please check...`.
pub(crate) async fn cannot_login(context: &Context, user: impl AsRef<str>) -> String {
translated(context, StockMessage::CannotLogin)

View File

@@ -1,506 +0,0 @@
//! # Synchronize items between devices.
use crate::chat::{Chat, ChatId};
use crate::config::Config;
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_SELF};
use crate::context::Context;
use crate::dc_tools::time;
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
use crate::token::Namespace;
use crate::{chat, stock_str, token};
use anyhow::Result;
use itertools::Itertools;
use lettre_email::mime::{self};
use lettre_email::PartBuilder;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct QrTokenData {
pub(crate) invitenumber: String,
pub(crate) auth: String,
pub(crate) grpid: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) enum SyncData {
AddQrToken(QrTokenData),
DeleteQrToken(QrTokenData),
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct SyncItem {
timestamp: i64,
data: SyncData,
}
#[derive(Debug, Deserialize)]
pub(crate) struct SyncItems {
items: Vec<SyncItem>,
}
impl Context {
/// Checks if sync messages shall be sent.
/// Receiving sync messages is currently always enabled;
/// the messages are force-encrypted anyway.
async fn is_sync_sending_enabled(&self) -> Result<bool> {
self.get_config_bool(Config::SendSyncMsgs).await
}
/// Adds an item to the list of items that should be synchronized to other devices.
pub(crate) async fn add_sync_item(&self, data: SyncData) -> Result<()> {
self.add_sync_item_with_timestamp(data, time()).await
}
/// Adds item and timestamp to the list of items that should be synchronized to other devices.
/// If device synchronization is disabled, the function does nothing.
async fn add_sync_item_with_timestamp(&self, data: SyncData, timestamp: i64) -> Result<()> {
if !self.is_sync_sending_enabled().await? {
return Ok(());
}
let item = SyncItem { timestamp, data };
let item = serde_json::to_string(&item)?;
self.sql
.execute(
"INSERT INTO multi_device_sync (item) VALUES(?);",
paramsv![item],
)
.await?;
Ok(())
}
/// Adds most recent qr-code tokens for a given chat to the list of items to be synced.
/// If device synchronization is disabled,
/// no tokens exist or the chat is unpromoted, the function does nothing.
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
if !self.is_sync_sending_enabled().await? {
return Ok(());
}
if let (Some(invitenumber), Some(auth)) = (
token::lookup(self, Namespace::InviteNumber, chat_id).await?,
token::lookup(self, Namespace::Auth, chat_id).await?,
) {
let grpid = if let Some(chat_id) = chat_id {
let chat = Chat::load_from_db(self, chat_id).await?;
if !chat.is_promoted() {
info!(
self,
"group '{}' not yet promoted, do not sync tokens yet.", chat.grpid
);
return Ok(());
}
Some(chat.grpid)
} else {
None
};
self.add_sync_item(SyncData::AddQrToken(QrTokenData {
invitenumber,
auth,
grpid,
}))
.await?;
}
Ok(())
}
// Add deleted qr-code token to the list of items to be synced
// so that the token also gets deleted on the other devices.
pub(crate) async fn sync_qr_code_token_deletion(
&self,
invitenumber: String,
auth: String,
) -> Result<()> {
self.add_sync_item(SyncData::DeleteQrToken(QrTokenData {
invitenumber,
auth,
grpid: None,
}))
.await
}
/// Sends out a self-sent message with items to be synchronized, if any.
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
if let Some((json, ids)) = self.build_sync_json().await? {
let chat_id =
ChatId::create_for_contact_with_blocked(self, DC_CONTACT_ID_SELF, Blocked::Yes)
.await?;
let mut msg = Message {
chat_id,
viewtype: Viewtype::Text,
text: Some(stock_str::sync_msg_body(self).await),
hidden: true,
subject: stock_str::sync_msg_subject(self).await,
..Default::default()
};
msg.param.set_cmd(SystemMessage::MultiDeviceSync);
msg.param.set(Param::Arg, json);
msg.param.set(Param::Arg2, ids);
msg.param.set_int(Param::GuaranteeE2ee, 1);
Ok(Some(chat::send_msg(self, chat_id, &mut msg).await?))
} else {
Ok(None)
}
}
/// Copies all sync items to a JSON string and clears the sync-table.
/// Returns the JSON string and a comma-separated string of the IDs used.
pub(crate) async fn build_sync_json(&self) -> Result<Option<(String, String)>> {
let (ids, serialized) = self
.sql
.query_map(
"SELECT id, item FROM multi_device_sync ORDER BY id;",
paramsv![],
|row| Ok((row.get::<_, u32>(0)?, row.get::<_, String>(1)?)),
|rows| {
let mut ids = vec![];
let mut serialized = String::default();
for row in rows {
let (id, item) = row?;
ids.push(id);
if !serialized.is_empty() {
serialized.push_str(",\n");
}
serialized.push_str(&item);
}
Ok((ids, serialized))
},
)
.await?;
if ids.is_empty() {
Ok(None)
} else {
Ok(Some((
format!("{{\"items\":[\n{}\n]}}", serialized),
ids.iter().map(|x| x.to_string()).join(","),
)))
}
}
pub(crate) async fn build_sync_part(&self, json: String) -> PartBuilder {
PartBuilder::new()
.content_type(&"application/json".parse::<mime::Mime>().unwrap())
.header((
"Content-Disposition",
"attachment; filename=\"multi-device-sync.json\"",
))
.body(json)
}
/// Deletes IDs as returned by `build_sync_json()`.
pub(crate) async fn delete_sync_ids(&self, ids: String) -> Result<()> {
self.sql
.execute(
format!("DELETE FROM multi_device_sync WHERE id IN ({});", ids),
paramsv![],
)
.await?;
Ok(())
}
/// Takes a JSON string created by `build_sync_json()`
/// and construct `SyncItems` from it.
pub(crate) async fn parse_sync_items(&self, serialized: String) -> Result<SyncItems> {
let sync_items: SyncItems = serde_json::from_str(&serialized)?;
Ok(sync_items)
}
/// Execute sync items.
///
/// CAVE: When changing the code to handle other sync items,
/// take care that does not result in calls to `add_sync_item()`
/// as otherwise we would add in a dead-loop between two devices
/// sending message back and forth.
///
/// If an error is returned, the caller shall not try over.
/// Therefore, errors should only be returned on database errors or so.
/// If eg. just an item cannot be deleted,
/// that should not hold off the other items to be executed.
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) -> Result<()> {
info!(self, "executing {} sync item(s)", items.items.len());
for item in &items.items {
match &item.data {
AddQrToken(token) => {
let chat_id = if let Some(grpid) = &token.grpid {
if let Some((chat_id, _, _)) =
chat::get_chat_id_by_grpid(self, grpid).await?
{
Some(chat_id)
} else {
warn!(
self,
"Ignoring token for nonexistent/deleted group '{}'.", grpid
);
continue;
}
} else {
None
};
token::save(self, Namespace::InviteNumber, chat_id, &token.invitenumber)
.await?;
token::save(self, Namespace::Auth, chat_id, &token.auth).await?;
}
DeleteQrToken(token) => {
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
token::delete(self, Namespace::Auth, &token.auth).await?;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::Chat;
use crate::chatlist::Chatlist;
use crate::test_utils::TestContext;
use crate::token::Namespace;
use anyhow::bail;
#[async_std::test]
async fn test_is_sync_sending_enabled() -> Result<()> {
let t = TestContext::new_alice().await;
assert!(!t.is_sync_sending_enabled().await?);
t.set_config_bool(Config::SendSyncMsgs, true).await?;
assert!(t.is_sync_sending_enabled().await?);
t.set_config_bool(Config::SendSyncMsgs, false).await?;
assert!(!t.is_sync_sending_enabled().await?);
Ok(())
}
#[async_std::test]
async fn test_build_sync_json() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config_bool(Config::SendSyncMsgs, true).await?;
assert!(t.build_sync_json().await?.is_none());
t.add_sync_item_with_timestamp(
SyncData::AddQrToken(QrTokenData {
invitenumber: "testinvite".to_string(),
auth: "testauth".to_string(),
grpid: Some("group123".to_string()),
}),
1631781316,
)
.await?;
t.add_sync_item_with_timestamp(
SyncData::DeleteQrToken(QrTokenData {
invitenumber: "123!?\":.;{}".to_string(),
auth: "456".to_string(),
grpid: None,
}),
1631781317,
)
.await?;
let (serialized, ids) = t.build_sync_json().await?.unwrap();
assert_eq!(
serialized,
r#"{"items":[
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"testinvite","auth":"testauth","grpid":"group123"}}},
{"timestamp":1631781317,"data":{"DeleteQrToken":{"invitenumber":"123!?\":.;{}","auth":"456","grpid":null}}}
]}"#
);
assert!(t.build_sync_json().await?.is_some());
t.delete_sync_ids(ids).await?;
assert!(t.build_sync_json().await?.is_none());
let sync_items = t.parse_sync_items(serialized).await?;
assert_eq!(sync_items.items.len(), 2);
Ok(())
}
#[async_std::test]
async fn test_build_sync_json_sync_msgs_off() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config_bool(Config::SendSyncMsgs, false).await?;
t.add_sync_item(SyncData::AddQrToken(QrTokenData {
invitenumber: "testinvite".to_string(),
auth: "testauth".to_string(),
grpid: Some("group123".to_string()),
}))
.await?;
assert!(t.build_sync_json().await?.is_none());
Ok(())
}
#[async_std::test]
async fn test_parse_sync_items() -> Result<()> {
let t = TestContext::new_alice().await;
assert!(t
.parse_sync_items(r#"{bad json}"#.to_string())
.await
.is_err());
assert!(t
.parse_sync_items(r#"{"badname":[]}"#.to_string())
.await
.is_err());
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#
.to_string(),
)
.await.is_err());
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#.to_string(),
)
.await
.is_err()); // `123` is invalid for `String`
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#.to_string(),
)
.await
.is_err()); // `true` is invalid for `String`
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#.to_string(),
)
.await
.is_err()); // `[]` is invalid for `String`
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#.to_string(),
)
.await
.is_err()); // `{}` is invalid for `String`
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#.to_string(),
)
.await
.is_err()); // missing field
// empty item list is okay
assert_eq!(
t.parse_sync_items(r#"{"items":[]}"#.to_string())
.await?
.items
.len(),
0
);
// to allow forward compatibility, additional fields should not break parsing
let sync_items = t
.parse_sync_items(
r#"{"items":[
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"yip","grpid":null}}},
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip","additional":123,"grpid":null}}}
]}"#
.to_string(),
)
.await?;
assert_eq!(sync_items.items.len(), 2);
let sync_items = t
.parse_sync_items(
r#"{"items":[
{"timestamp":1631781318,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip","grpid":null}}}
],"additional":"field"}"#
.to_string(),
)
.await?;
assert_eq!(sync_items.items.len(), 1);
if let AddQrToken(token) = &sync_items.items.get(0).unwrap().data {
assert_eq!(token.invitenumber, "in");
assert_eq!(token.auth, "yip");
assert_eq!(token.grpid, None);
} else {
bail!("bad item");
}
// to allow backward compatibility, missing `Option<>` should not break parsing
let sync_items = t.parse_sync_items(
r#"{"items":[{"timestamp":1631781319,"data":{"AddQrToken":{"invitenumber":"in","auth":"a"}}}]}"#.to_string(),
)
.await?;
assert_eq!(sync_items.items.len(), 1);
Ok(())
}
#[async_std::test]
async fn test_execute_sync_items() -> Result<()> {
let t = TestContext::new_alice().await;
assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await);
let sync_items = t
.parse_sync_items(
r#"{"items":[
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"yip-in","auth":"a"}}},
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"delete unexistant, shall continue"}}},
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip-auth"}}},
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"foo","grpid":"non-existant"}}},
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"directly deleted"}}},
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"directly deleted"}}}
]}"#
.to_string(),
)
.await?;
t.execute_sync_items(&sync_items).await?;
assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await);
assert!(token::exists(&t, Namespace::Auth, "yip-auth").await);
assert!(!token::exists(&t, Namespace::Auth, "non-existant").await);
assert!(!token::exists(&t, Namespace::Auth, "directly deleted").await);
Ok(())
}
#[async_std::test]
async fn test_send_sync_msg() -> Result<()> {
let alice = TestContext::new_alice().await;
alice.set_config_bool(Config::SendSyncMsgs, true).await?;
alice
.add_sync_item(SyncData::AddQrToken(QrTokenData {
invitenumber: "in".to_string(),
auth: "testtoken".to_string(),
grpid: None,
}))
.await?;
let msg_id = alice.send_sync_msg().await?.unwrap();
let msg = Message::load_from_db(&alice, msg_id).await?;
let chat = Chat::load_from_db(&alice, msg.chat_id).await?;
assert!(chat.is_self_talk());
// check that the used self-talk is not visible to the user
// but that creation will still work (in this case, the chat is empty)
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
let chat_id = ChatId::create_for_contact(&alice, DC_CONTACT_ID_SELF).await?;
let chat = Chat::load_from_db(&alice, chat_id).await?;
assert!(chat.is_self_talk());
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
let msgs = chat::get_chat_msgs(&alice, chat_id, 0, None).await?;
assert_eq!(msgs.len(), 0);
// let alice's other device receive and execute the sync message,
// also here, self-talk should stay hidden
let sent_msg = alice.pop_sent_msg().await;
let alice2 = TestContext::new_alice().await;
alice2.recv_msg(&sent_msg).await;
assert!(token::exists(&alice2, token::Namespace::Auth, "testtoken").await);
assert_eq!(Chatlist::try_load(&alice2, 0, None, None).await?.len(), 0);
// the same sync message sent to bob must not be executed
let bob = TestContext::new_bob().await;
bob.recv_msg(&sent_msg).await;
assert!(!token::exists(&bob, token::Namespace::Auth, "testtoken").await);
Ok(())
}
}

View File

@@ -34,9 +34,6 @@ use crate::message::{update_msg_state, Message, MessageState, MsgId};
use crate::mimeparser::MimeMessage;
use crate::param::{Param, Params};
#[allow(non_upper_case_globals)]
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
type EventSink =
dyn Fn(Event) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> + Send + Sync + 'static;
@@ -111,15 +108,12 @@ impl TestContext {
let (evtracker_sender, evtracker_receiver) = channel::unbounded();
async_std::task::spawn(async move {
// Make sure that the test fails if there is a panic on this thread here
// (but not if there is a panic on another thread)
let looptask_id = task::current().id();
// Make sure that the test fails if there is a panic on this thread here:
let current_id = task::current().id();
let orig_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
if let Some(panicked_task) = task::try_current() {
if panicked_task.id() == looptask_id {
poison_sender.try_send(panic_info.to_string()).ok();
}
if task::current().id() == current_id {
poison_sender.try_send(panic_info.to_string()).ok();
}
orig_hook(panic_info);
}));
@@ -593,21 +587,6 @@ impl EvTracker {
}
}
}
pub async fn get_matching<F: Fn(EventType) -> bool>(&self, event_matcher: F) -> EventType {
const TIMEOUT: Duration = Duration::from_secs(20);
loop {
let event = async_std::future::timeout(TIMEOUT, self.recv())
.await
.unwrap()
.unwrap();
if event_matcher(event.clone()) {
return event;
}
}
}
}
impl Deref for EvTracker {