mirror of
https://github.com/chatmail/core.git
synced 2026-06-30 03:26:36 +03:00
Compare commits
1 Commits
1.46.0
...
bench-bott
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f69ede0de |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --workspace --tests --examples
|
||||
args: --all
|
||||
|
||||
|
||||
build_and_test:
|
||||
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,46 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 1.46.0
|
||||
|
||||
- breaking change: `dc_configure()` report errors in
|
||||
`DC_EVENT_CONFIGURE_PROGRESS`: capturing error events is no longer working
|
||||
#1886 #1905
|
||||
|
||||
- breaking change: removed `DC_LP_{IMAP|SMTP}_SOCKET*` from `server_flags`;
|
||||
added `mail_security` and `send_security` using `DC_SOCKET` enum #1835
|
||||
|
||||
- parse multiple servers in Mozilla autoconfig #1860
|
||||
|
||||
- try multiple servers for each protocol #1871
|
||||
|
||||
- do IMAP and SMTP configuration in parallel #1891
|
||||
|
||||
- configuration cleanup and speedup #1858 #1875 #1889 #1904 #1906
|
||||
|
||||
- secure-join cleanup, testing, fixing #1876 #1877 #1887 #1888 #1896 #1899 #1900
|
||||
|
||||
- do not reset peerstate on encrypted messages,
|
||||
ignore reordered autocrypt headers #1885 #1890
|
||||
|
||||
- always sort message replies after parent message #1852
|
||||
|
||||
- add an index to significantly speed up `get_fresh_msg_cnt()` #1881
|
||||
|
||||
- improve mimetype guessing for PDF and many other formats #1857 #1861
|
||||
|
||||
- improve accepting invalid html #1851
|
||||
|
||||
- improve tests, cleanup and ci #1850 #1856 #1859 #1861 #1884 #1894 #1895
|
||||
|
||||
- tweak HELO command #1908
|
||||
|
||||
- make `dc_accounts_get_all()` return accounts sorted #1909
|
||||
|
||||
- fix KML coordinates precision used for location streaming #1872
|
||||
|
||||
- fix cancelling import/export #1855
|
||||
|
||||
|
||||
## 1.45.0
|
||||
|
||||
- add `dc_accounts_t` account manager object and related api functions #1784
|
||||
|
||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -200,7 +200,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "async-smtp"
|
||||
version = "0.3.4"
|
||||
source = "git+https://github.com/async-email/async-smtp?rev=2275fd8d13e39b2c58d6605c786ff06ff9e05708#2275fd8d13e39b2c58d6605c786ff06ff9e05708"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a243eb83ec706b4bed429fc6917e8a103edb8c083720e5347a57a2766ffd41c"
|
||||
dependencies = [
|
||||
"async-native-tls",
|
||||
"async-std",
|
||||
@@ -803,7 +804,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.46.0"
|
||||
version = "1.45.0"
|
||||
dependencies = [
|
||||
"ansi_term 0.12.1",
|
||||
"anyhow",
|
||||
@@ -878,7 +879,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.46.0"
|
||||
version = "1.45.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.46.0"
|
||||
version = "1.45.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
@@ -20,7 +20,7 @@ smallvec = "1.0.0"
|
||||
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
||||
num-derive = "0.3.0"
|
||||
num-traits = "0.2.6"
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
|
||||
async-smtp = "0.3"
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
async-imap = "0.4.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.46.0"
|
||||
version = "1.45.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -1492,6 +1492,17 @@ char* dc_get_mime_headers (dc_context_t* context, uint32_t ms
|
||||
*/
|
||||
void dc_delete_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||
|
||||
/*
|
||||
* Empty IMAP server folder: delete all messages.
|
||||
* Deprecated, use dc_set_config() with the key "delete_server_after" instead.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param flags What to delete, a combination of the @ref DC_EMPTY flags
|
||||
* @return None.
|
||||
*/
|
||||
void dc_empty_server (dc_context_t* context, uint32_t flags);
|
||||
|
||||
|
||||
/**
|
||||
* Forward messages to another chat.
|
||||
@@ -2360,7 +2371,7 @@ dc_context_t* dc_accounts_get_account (dc_accounts_t* accounts, uint32
|
||||
|
||||
/**
|
||||
* Get the currently selected account.
|
||||
* If there is at least one account in the account-manager,
|
||||
* If there is at least once account in the account-manager,
|
||||
* there is always a selected one.
|
||||
* To change the selected account, use dc_accounts_select_account();
|
||||
* also adding/importing/migrating accounts may change the selection.
|
||||
@@ -4270,6 +4281,10 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*/
|
||||
|
||||
|
||||
#define DC_EMPTY_MVBOX 0x01 // Deprecated, flag for dc_empty_server(): Clear all mvbox messages
|
||||
#define DC_EMPTY_INBOX 0x02 // Deprecated, flag for dc_empty_server(): Clear all INBOX messages
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_event_emitter_t
|
||||
*
|
||||
@@ -4491,6 +4506,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
|
||||
|
||||
/**
|
||||
* Emitted when an IMAP folder was emptied.
|
||||
*
|
||||
* @param data1 0
|
||||
* @param data2 (char*) Folder name.
|
||||
*/
|
||||
#define DC_EVENT_IMAP_FOLDER_EMPTIED 106
|
||||
|
||||
/**
|
||||
* Emitted when a new blob file was successfully written
|
||||
*
|
||||
@@ -4669,7 +4692,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
* Inform about the configuration progress started by dc_configure().
|
||||
*
|
||||
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
* @param data2 (char*) progress comment, error message or NULL if not applicable
|
||||
* @param data2 0
|
||||
*/
|
||||
#define DC_EVENT_CONFIGURE_PROGRESS 2041
|
||||
|
||||
@@ -4732,8 +4755,18 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
|
||||
|
||||
#define DC_EVENT_FILE_COPIED 2055 // not used anymore
|
||||
#define DC_EVENT_IS_OFFLINE 2081 // not used anymore
|
||||
#define DC_EVENT_GET_STRING 2091 // not used anymore, use dc_set_stock_translation()
|
||||
#define DC_ERROR_SEE_STRING 0 // not used anymore
|
||||
#define DC_ERROR_SELF_NOT_IN_GROUP 1 // not used anymore
|
||||
#define DC_STR_SELFNOTINGRP 21 // not used anymore
|
||||
#define DC_EVENT_DATA1_IS_STRING(e) 0 // not used anymore
|
||||
#define DC_EVENT_DATA2_IS_STRING(e) ((e)==DC_EVENT_CONFIGURE_PROGRESS || (e)==DC_EVENT_IMEX_FILE_WRITTEN || ((e)>=100 && (e)<=499))
|
||||
#define DC_EVENT_DATA2_IS_STRING(e) ((e)==DC_EVENT_IMEX_FILE_WRITTEN || ((e)>=100 && (e)<=499))
|
||||
#define DC_EVENT_RETURNS_INT(e) ((e)==DC_EVENT_IS_OFFLINE) // not used anymore
|
||||
#define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore
|
||||
#define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore
|
||||
#define dc_chat_get_archived(a) (dc_chat_get_visibility((a))==1? 1 : 0) // not used anymore
|
||||
|
||||
|
||||
/*
|
||||
@@ -4924,9 +4957,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
|
||||
#define DC_STR_VIDEOCHAT_INVITATION 82
|
||||
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
|
||||
#define DC_STR_CONFIGURATION_FAILED 84
|
||||
|
||||
#define DC_STR_COUNT 84
|
||||
#define DC_STR_COUNT 83
|
||||
|
||||
/*
|
||||
* @}
|
||||
|
||||
@@ -355,6 +355,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::SmtpMessageSent(_)
|
||||
| EventType::ImapMessageDeleted(_)
|
||||
| EventType::ImapMessageMoved(_)
|
||||
| EventType::ImapFolderEmptied(_)
|
||||
| EventType::NewBlobFile(_)
|
||||
| EventType::DeletedBlobFile(_)
|
||||
| EventType::Warning(_)
|
||||
@@ -372,7 +373,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
let id = id.unwrap_or_default();
|
||||
id as libc::c_int
|
||||
}
|
||||
EventType::ConfigureProgress { progress, .. } | EventType::ImexProgress(progress) => {
|
||||
EventType::ConfigureProgress(progress) | EventType::ImexProgress(progress) => {
|
||||
*progress as libc::c_int
|
||||
}
|
||||
EventType::ImexFileWritten(_) => 0,
|
||||
@@ -397,6 +398,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::SmtpMessageSent(_)
|
||||
| EventType::ImapMessageDeleted(_)
|
||||
| EventType::ImapMessageMoved(_)
|
||||
| EventType::ImapFolderEmptied(_)
|
||||
| EventType::NewBlobFile(_)
|
||||
| EventType::DeletedBlobFile(_)
|
||||
| EventType::Warning(_)
|
||||
@@ -405,7 +407,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::ContactsChanged(_)
|
||||
| EventType::LocationChanged(_)
|
||||
| EventType::ConfigureProgress { .. }
|
||||
| EventType::ConfigureProgress(_)
|
||||
| EventType::ImexProgress(_)
|
||||
| EventType::ImexFileWritten(_)
|
||||
| EventType::ChatModified(_) => 0,
|
||||
@@ -436,6 +438,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::SmtpMessageSent(msg)
|
||||
| EventType::ImapMessageDeleted(msg)
|
||||
| EventType::ImapMessageMoved(msg)
|
||||
| EventType::ImapFolderEmptied(msg)
|
||||
| EventType::NewBlobFile(msg)
|
||||
| EventType::DeletedBlobFile(msg)
|
||||
| EventType::Warning(msg)
|
||||
@@ -453,17 +456,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ContactsChanged(_)
|
||||
| EventType::LocationChanged(_)
|
||||
| EventType::ConfigureProgress(_)
|
||||
| EventType::ImexProgress(_)
|
||||
| EventType::SecurejoinInviterProgress { .. }
|
||||
| EventType::SecurejoinJoinerProgress { .. }
|
||||
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
EventType::ImexFileWritten(file) => {
|
||||
let data2 = file.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
@@ -1426,6 +1423,17 @@ pub unsafe extern "C" fn dc_delete_msgs(
|
||||
block_on(message::delete_msgs(&ctx, &msg_ids))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_empty_server(context: *mut dc_context_t, flags: u32) {
|
||||
if context.is_null() || flags == 0 {
|
||||
eprintln!("ignoring careless call to dc_empty_server()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(message::dc_empty_server(&ctx, flags))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_forward_msgs(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1883,13 +1891,8 @@ pub unsafe extern "C" fn dc_join_securejoin(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
securejoin::dc_join_securejoin(&ctx, &to_string_lossy(qr))
|
||||
.await
|
||||
.map(|chatid| chatid.to_u32())
|
||||
.log_err(ctx, "failed dc_join_securejoin() call")
|
||||
.unwrap_or_default()
|
||||
})
|
||||
block_on(async move { securejoin::dc_join_securejoin(&ctx, &to_string_lossy(qr)).await })
|
||||
.to_u32()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -405,6 +405,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
event <event-id to test>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
|
||||
clear -- clear screen\n\
|
||||
exit or quit\n\
|
||||
============================================="
|
||||
@@ -1091,6 +1092,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
seconds, device_cnt, server_cnt
|
||||
);
|
||||
}
|
||||
"emptyserver" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <flags> missing");
|
||||
|
||||
message::dc_empty_server(&context, arg1.parse()?).await;
|
||||
}
|
||||
"" => (),
|
||||
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
|
||||
}
|
||||
|
||||
@@ -80,21 +80,11 @@ fn receive_event(event: EventType) {
|
||||
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
|
||||
);
|
||||
}
|
||||
EventType::ConfigureProgress { progress, comment } => {
|
||||
if let Some(comment) = comment {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!(
|
||||
"Received CONFIGURE_PROGRESS({} ‰, {})",
|
||||
progress, comment
|
||||
))
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
|
||||
);
|
||||
}
|
||||
EventType::ConfigureProgress(progress) => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
|
||||
);
|
||||
}
|
||||
EventType::ImexProgress(progress) => {
|
||||
info!(
|
||||
@@ -420,7 +410,7 @@ async fn handle_cmd(
|
||||
"joinqr" => {
|
||||
ctx.start_io().await;
|
||||
if !arg0.is_empty() {
|
||||
dc_join_securejoin(&ctx, arg1).await?;
|
||||
dc_join_securejoin(&ctx, arg1).await;
|
||||
}
|
||||
}
|
||||
"exit" | "quit" => return Ok(ExitResult::Exit),
|
||||
|
||||
@@ -10,7 +10,7 @@ use deltachat::EventType;
|
||||
|
||||
fn cb(event: EventType) {
|
||||
match event {
|
||||
EventType::ConfigureProgress { progress, .. } => {
|
||||
EventType::ConfigureProgress(progress) => {
|
||||
log::info!("progress: {}", progress);
|
||||
}
|
||||
EventType::Info(msg) => {
|
||||
|
||||
@@ -179,6 +179,17 @@ class Account(object):
|
||||
if not self.is_configured():
|
||||
raise ValueError("need to configure first")
|
||||
|
||||
def empty_server_folders(self, inbox=False, mvbox=False):
|
||||
""" empty server folders. """
|
||||
flags = 0
|
||||
if inbox:
|
||||
flags |= const.DC_EMPTY_INBOX
|
||||
if mvbox:
|
||||
flags |= const.DC_EMPTY_MVBOX
|
||||
if not flags:
|
||||
raise ValueError("no flags set")
|
||||
lib.dc_empty_server(self._dc_context, flags)
|
||||
|
||||
def get_latest_backupfile(self, backupdir):
|
||||
""" return the latest backup file in a given directory.
|
||||
"""
|
||||
|
||||
@@ -17,8 +17,7 @@ def iter_array(dc_array_t, constructor):
|
||||
|
||||
|
||||
def from_dc_charpointer(obj):
|
||||
if obj != ffi.NULL:
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
|
||||
|
||||
class DCLot:
|
||||
|
||||
@@ -819,12 +819,18 @@ class TestOnlineAccount:
|
||||
assert msg_in.text == "message2"
|
||||
assert msg_in.is_forwarded()
|
||||
|
||||
def test_send_self_message(self, acfactory, lp):
|
||||
def test_send_self_message_and_empty_folder(self, acfactory, lp):
|
||||
ac1 = acfactory.get_one_online_account(mvbox=True, move=True)
|
||||
lp.sec("ac1: create self chat")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
chat.send_text("hello")
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
ac1.empty_server_folders(inbox=True, mvbox=True)
|
||||
ev1 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED")
|
||||
ev2 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED")
|
||||
boxes = [ev1.data2, ev2.data2]
|
||||
boxes.remove("INBOX")
|
||||
assert len(boxes) == 1 and boxes[0].endswith("DeltaChat")
|
||||
|
||||
def test_send_and_receive_message_markseen(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
@@ -1892,7 +1898,8 @@ class TestOnlineConfigureFails:
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_progress(500)
|
||||
configtracker.wait_progress(0)
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
|
||||
assert "cannot login" in ev.data2.lower()
|
||||
|
||||
def test_invalid_user(self, acfactory):
|
||||
ac1, configdict = acfactory.get_online_config()
|
||||
@@ -1900,7 +1907,8 @@ class TestOnlineConfigureFails:
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_progress(500)
|
||||
configtracker.wait_progress(0)
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
|
||||
assert "cannot login" in ev.data2.lower()
|
||||
|
||||
def test_invalid_domain(self, acfactory):
|
||||
ac1, configdict = acfactory.get_online_config()
|
||||
@@ -1908,4 +1916,5 @@ class TestOnlineConfigureFails:
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_progress(500)
|
||||
configtracker.wait_progress(0)
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
|
||||
assert "could not connect" in ev.data2.lower()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::task::{Context as TaskContext, Poll};
|
||||
@@ -20,7 +20,7 @@ use crate::events::Event;
|
||||
pub struct Accounts {
|
||||
dir: PathBuf,
|
||||
config: Config,
|
||||
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
|
||||
accounts: Arc<RwLock<HashMap<u32, Context>>>,
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
@@ -342,9 +342,9 @@ impl Config {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
|
||||
pub async fn load_accounts(&self) -> Result<HashMap<u32, Context>> {
|
||||
let cfg = &*self.inner.read().await;
|
||||
let mut accounts = BTreeMap::new();
|
||||
let mut accounts = HashMap::with_capacity(cfg.accounts.len());
|
||||
for account_config in &cfg.accounts {
|
||||
let ctx = Context::new(
|
||||
cfg.os_name.clone(),
|
||||
@@ -530,23 +530,4 @@ mod tests {
|
||||
ctx.get_config(crate::config::Config::Addr).await.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that accounts are sorted by ID.
|
||||
#[async_std::test]
|
||||
async fn test_accounts_sorted() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 2..10 {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, expected_id);
|
||||
}
|
||||
|
||||
let ids = accounts.get_all().await;
|
||||
for (i, expected_id) in (1..10).enumerate() {
|
||||
assert_eq!(ids.get(i), Some(&expected_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
src/blob.rs
12
src/blob.rs
@@ -685,7 +685,7 @@ mod tests {
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
|
||||
assert!(stem.contains("foo"));
|
||||
assert!(!stem.contains('?'));
|
||||
assert!(!stem.contains("?"));
|
||||
assert_eq!(ext, ".bar");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("no-extension");
|
||||
@@ -698,10 +698,10 @@ mod tests {
|
||||
assert!(!stem.contains("ignored"));
|
||||
assert!(stem.contains("this"));
|
||||
assert!(stem.contains("forbidden"));
|
||||
assert!(!stem.contains('/'));
|
||||
assert!(!stem.contains('\\'));
|
||||
assert!(!stem.contains(':'));
|
||||
assert!(!stem.contains('*'));
|
||||
assert!(!stem.contains('?'));
|
||||
assert!(!stem.contains("/"));
|
||||
assert!(!stem.contains("\\"));
|
||||
assert!(!stem.contains(":"));
|
||||
assert!(!stem.contains("*"));
|
||||
assert!(!stem.contains("?"));
|
||||
}
|
||||
}
|
||||
|
||||
36
src/chat.rs
36
src/chat.rs
@@ -842,11 +842,7 @@ impl Chat {
|
||||
/* check if we want to encrypt this message. If yes and circumstances change
|
||||
so that E2EE is no longer available at a later point (reset, changed settings),
|
||||
we might not send the message out at all */
|
||||
if !msg
|
||||
.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if msg.param.get_int(Param::ForcePlaintext).unwrap_or_default() == 0 {
|
||||
let mut can_encrypt = true;
|
||||
let mut all_mutual = context.get_config_bool(Config::E2eeEnabled).await;
|
||||
|
||||
@@ -1170,7 +1166,7 @@ pub async fn create_by_msg_id(context: &Context, msg_id: MsgId) -> Result<ChatId
|
||||
}
|
||||
|
||||
/// Create a normal chat with a single user. To create group chats,
|
||||
/// see [Chat::create_group_chat].
|
||||
/// see dc_create_group_chat().
|
||||
///
|
||||
/// If a chat already exists, this ID is returned, otherwise a new chat is created;
|
||||
/// this new chat may already contain messages, eg. from the deaddrop, to get the
|
||||
@@ -2701,7 +2697,6 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a tuple of `(chatid, is_verified, blocked)`.
|
||||
pub(crate) async fn get_chat_id_by_grpid(
|
||||
context: &Context,
|
||||
grpid: impl AsRef<str>,
|
||||
@@ -2891,6 +2886,7 @@ pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat;
|
||||
use crate::contact::Contact;
|
||||
use crate::test_utils::*;
|
||||
|
||||
@@ -3553,4 +3549,30 @@ mod tests {
|
||||
chat_id.set_draft(&t.ctx, Some(&mut msg)).await;
|
||||
assert!(!chat_id.parent_is_encrypted(&t.ctx).await.unwrap());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_fresh_msg_cnt() {
|
||||
let t = TestContext::new().await;
|
||||
// create 50 chats with 1k messages each
|
||||
let mut chat_ids = Vec::new();
|
||||
for _i in 1..50 {
|
||||
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
for _i in 1..1000 {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some("message text".to_string());
|
||||
chat::send_msg(&t.ctx, chat_id, &mut msg)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
}
|
||||
chat_ids.push(chat_id);
|
||||
}
|
||||
// load all chats 20k times
|
||||
for _i in 1..20_000 {
|
||||
for chat_id in chat_ids.iter() {
|
||||
chat_id.get_fresh_msg_cnt(&t.ctx).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,8 +268,6 @@ pub(crate) async fn moz_autoconfigure(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -209,8 +209,6 @@ pub(crate) async fn outlk_autodiscover(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,7 +7,6 @@ mod server_params;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_std::prelude::*;
|
||||
use async_std::task;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -20,26 +19,19 @@ use crate::message::Message;
|
||||
use crate::oauth2::*;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::{chat, e2ee, provider};
|
||||
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use server_params::{expand_param_vector, ServerParams};
|
||||
use server_params::ServerParams;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
($context:tt, $progress:expr) => {
|
||||
assert!(
|
||||
$progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.emit_event($crate::events::EventType::ConfigureProgress {
|
||||
progress: $progress,
|
||||
comment: $comment,
|
||||
});
|
||||
};
|
||||
($context:tt, $progress:expr) => {
|
||||
progress!($context, $progress, None);
|
||||
$context.emit_event($crate::events::EventType::ConfigureProgress($progress));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,17 +110,7 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
progress!(
|
||||
self,
|
||||
0,
|
||||
Some(
|
||||
self.stock_string_repl_str(
|
||||
StockMessage::ConfigurationFailed,
|
||||
err.to_string(),
|
||||
)
|
||||
.await
|
||||
)
|
||||
);
|
||||
progress!(self, 0);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
@@ -211,8 +193,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
progress!(ctx, 500);
|
||||
|
||||
let servers = expand_param_vector(
|
||||
param_autoconfig.unwrap_or_else(|| {
|
||||
let servers: Vec<ServerParams> = param_autoconfig
|
||||
.unwrap_or_else(|| {
|
||||
vec![
|
||||
ServerParams {
|
||||
protocol: Protocol::IMAP,
|
||||
@@ -229,59 +211,27 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
username: param.smtp.user.clone(),
|
||||
},
|
||||
]
|
||||
}),
|
||||
¶m.addr,
|
||||
¶m_domain,
|
||||
);
|
||||
|
||||
progress!(ctx, 550);
|
||||
|
||||
// Spawn SMTP configuration task
|
||||
let mut smtp = Smtp::new();
|
||||
|
||||
let context_smtp = ctx.clone();
|
||||
let mut smtp_param = param.smtp.clone();
|
||||
let smtp_addr = param.addr.clone();
|
||||
let smtp_servers: Vec<ServerParams> = servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::SMTP)
|
||||
.cloned()
|
||||
})
|
||||
.into_iter()
|
||||
// The order of expansion is important: ports are expanded the
|
||||
// last, so they are changed the first. Username is only
|
||||
// changed if default value (address with domain) didn't work
|
||||
// for all available hosts and ports.
|
||||
.flat_map(|params| params.expand_usernames(¶m.addr).into_iter())
|
||||
.flat_map(|params| params.expand_hostnames(¶m_domain).into_iter())
|
||||
.flat_map(|params| params.expand_ports().into_iter())
|
||||
.collect();
|
||||
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp_configured = false;
|
||||
for smtp_server in smtp_servers {
|
||||
smtp_param.user = smtp_server.username.clone();
|
||||
smtp_param.server = smtp_server.hostname.clone();
|
||||
smtp_param.port = smtp_server.port;
|
||||
smtp_param.security = smtp_server.socket;
|
||||
|
||||
if try_smtp_one_param(&context_smtp, &smtp_param, &smtp_addr, oauth2, &mut smtp).await {
|
||||
smtp_configured = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if smtp_configured {
|
||||
Some(smtp_param)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
progress!(ctx, 600);
|
||||
|
||||
// Configure IMAP
|
||||
progress!(ctx, 600);
|
||||
let (_s, r) = async_std::sync::channel(1);
|
||||
let mut imap = Imap::new(r);
|
||||
|
||||
let mut imap_configured = false;
|
||||
let imap_servers: Vec<&ServerParams> = servers
|
||||
for imap_server in servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::IMAP)
|
||||
.collect();
|
||||
let imap_servers_count = imap_servers.len();
|
||||
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
|
||||
{
|
||||
param.imap.user = imap_server.username.clone();
|
||||
param.imap.server = imap_server.hostname.clone();
|
||||
param.imap.port = imap_server.port;
|
||||
@@ -291,21 +241,31 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
imap_configured = true;
|
||||
break;
|
||||
}
|
||||
progress!(
|
||||
ctx,
|
||||
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
|
||||
);
|
||||
}
|
||||
if !imap_configured {
|
||||
bail!("IMAP autoconfig did not succeed");
|
||||
}
|
||||
|
||||
progress!(ctx, 850);
|
||||
// Configure SMTP
|
||||
progress!(ctx, 750);
|
||||
let mut smtp = Smtp::new();
|
||||
|
||||
// Wait for SMTP configuration
|
||||
if let Some(smtp_param) = smtp_config_task.await {
|
||||
param.smtp = smtp_param;
|
||||
} else {
|
||||
let mut smtp_configured = false;
|
||||
for smtp_server in servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::SMTP)
|
||||
{
|
||||
param.smtp.user = smtp_server.username.clone();
|
||||
param.smtp.server = smtp_server.hostname.clone();
|
||||
param.smtp.port = smtp_server.port;
|
||||
param.smtp.security = smtp_server.socket;
|
||||
|
||||
if try_smtp_one_param(ctx, ¶m.smtp, ¶m.addr, oauth2, &mut smtp).await {
|
||||
smtp_configured = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !smtp_configured {
|
||||
bail!("SMTP autoconfig did not succeed");
|
||||
}
|
||||
|
||||
@@ -538,7 +498,6 @@ pub enum Error {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
use crate::config::*;
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::provider::{Protocol, Socket};
|
||||
///
|
||||
/// Can be loaded from offline provider database, online configuraiton
|
||||
/// or derived from user entered parameters.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ServerParams {
|
||||
/// Protocol, such as IMAP or SMTP.
|
||||
pub protocol: Protocol,
|
||||
@@ -113,52 +113,3 @@ impl ServerParams {
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands vector of `ServerParams`, replacing placeholders with
|
||||
/// variants to try.
|
||||
pub(crate) fn expand_param_vector(
|
||||
v: Vec<ServerParams>,
|
||||
addr: &str,
|
||||
domain: &str,
|
||||
) -> Vec<ServerParams> {
|
||||
v.into_iter()
|
||||
// The order of expansion is important: ports are expanded the
|
||||
// last, so they are changed the first. Username is only
|
||||
// changed if default value (address with domain) didn't work
|
||||
// for all available hosts and ports.
|
||||
.flat_map(|params| params.expand_usernames(addr).into_iter())
|
||||
.flat_map(|params| params.expand_hostnames(domain).into_iter())
|
||||
.flat_map(|params| params.expand_ports().into_iter())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_expand_param_vector() {
|
||||
let v = expand_param_vector(
|
||||
vec![ServerParams {
|
||||
protocol: Protocol::IMAP,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 0,
|
||||
socket: Socket::SSL,
|
||||
username: "foobar".to_string(),
|
||||
}],
|
||||
"foobar@example.net",
|
||||
"example.net",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
v,
|
||||
vec![ServerParams {
|
||||
protocol: Protocol::IMAP,
|
||||
hostname: "example.net".to_string(),
|
||||
port: 993,
|
||||
socket: Socket::SSL,
|
||||
username: "foobar".to_string(),
|
||||
}],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
//! # Constants
|
||||
#![allow(dead_code)]
|
||||
|
||||
use deltachat_derive::*;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -7,6 +9,13 @@ lazy_static! {
|
||||
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
|
||||
}
|
||||
|
||||
// some defaults
|
||||
const DC_E2EE_DEFAULT_ENABLED: i32 = 1;
|
||||
const DC_INBOX_WATCH_DEFAULT: i32 = 1;
|
||||
const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
|
||||
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
|
||||
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
@@ -109,11 +118,11 @@ pub const DC_GCL_ADD_SELF: usize = 0x02;
|
||||
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
|
||||
|
||||
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
|
||||
pub const DC_CHAT_ID_DEADDROP: u32 = 1;
|
||||
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
|
||||
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
|
||||
pub const DC_CHAT_ID_TRASH: u32 = 3;
|
||||
/// a message is just in creation but not yet assigned to a chat (eg. we may need the message ID to set up blobs; this avoids unready message to be sent and shown)
|
||||
pub const DC_CHAT_ID_MSGS_IN_CREATION: u32 = 4;
|
||||
const DC_CHAT_ID_MSGS_IN_CREATION: u32 = 4;
|
||||
/// virtual chat showing all messages flagged with msgs.starred=2
|
||||
pub const DC_CHAT_ID_STARRED: u32 = 5;
|
||||
/// only an indicator in a chatlist
|
||||
@@ -157,9 +166,9 @@ pub const DC_MSG_ID_DAYMARKER: u32 = 9;
|
||||
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
|
||||
|
||||
/// approx. max. length returned by dc_msg_get_text()
|
||||
pub const DC_MAX_GET_TEXT_LEN: usize = 30000;
|
||||
const DC_MAX_GET_TEXT_LEN: usize = 30000;
|
||||
/// approx. max. length returned by dc_get_msg_info()
|
||||
pub const DC_MAX_GET_INFO_LEN: usize = 100_000;
|
||||
const DC_MAX_GET_INFO_LEN: usize = 100_000;
|
||||
|
||||
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
|
||||
pub const DC_CONTACT_ID_SELF: u32 = 1;
|
||||
@@ -286,6 +295,16 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// These constants are used as events
|
||||
// reported to the callback given to dc_context_new().
|
||||
// If you do not want to handle an event, it is always safe to return 0,
|
||||
// so there is no need to add a "case" for every event.
|
||||
|
||||
const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
|
||||
const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
|
||||
const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
|
||||
const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
|
||||
|
||||
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
|
||||
@@ -573,7 +573,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir, 1).await;
|
||||
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into(), 1).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ use crate::message::{self, MessageState, MessengerMessage, MsgId};
|
||||
use crate::mimeparser::*;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
use crate::securejoin::{
|
||||
self, handle_securejoin_handshake, observe_securejoin_on_other_device, BobStatus,
|
||||
};
|
||||
use crate::stock::StockMessage;
|
||||
use crate::{contact, location};
|
||||
|
||||
@@ -416,6 +418,8 @@ async fn add_parts(
|
||||
}
|
||||
Err(err) => {
|
||||
*hidden = true;
|
||||
|
||||
context.bob.write().await.status = BobStatus::Error; // secure-join failed
|
||||
context.stop_ongoing().await;
|
||||
warn!(context, "Error in Secure-Join message handling: {}", err);
|
||||
return Ok(());
|
||||
@@ -2209,7 +2213,7 @@ mod tests {
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let msg = message::Message::load_from_db(&t.ctx, *msg_id)
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
@@ -2260,7 +2264,7 @@ mod tests {
|
||||
chat::get_chat_msgs(&t.ctx, group_id, 0, None).await.len(),
|
||||
1
|
||||
);
|
||||
let msg = message::Message::load_from_db(&t.ctx, *msg_id)
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.state, MessageState::OutMdnRcvd);
|
||||
@@ -2348,7 +2352,7 @@ mod tests {
|
||||
} else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let msg = message::Message::load_from_db(&t.ctx, *msg_id)
|
||||
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||
|
||||
@@ -639,8 +639,6 @@ pub(crate) fn improve_single_line_input(input: impl AsRef<str>) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
use std::convert::TryInto;
|
||||
|
||||
|
||||
153
src/e2ee.rs
153
src/e2ee.rs
@@ -135,31 +135,41 @@ pub async fn try_decrypt(
|
||||
.map(|from| from.addr)
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut peerstate = Peerstate::from_addr(context, &from).await?;
|
||||
let mut peerstate = None;
|
||||
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
|
||||
|
||||
if message_time > 0 {
|
||||
peerstate = Peerstate::from_addr(context, &from).await?;
|
||||
|
||||
// Apply Autocrypt header
|
||||
if let Some(ref header) = Aheader::from_headers(context, &from, &mail.headers) {
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
peerstate.apply_header(&header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
} else {
|
||||
if let Some(ref header) = autocryptheader {
|
||||
peerstate.apply_header(&header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
} else if message_time > peerstate.last_seen_autocrypt && !contains_report(mail) {
|
||||
peerstate.degrade_encryption(message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
}
|
||||
} else if let Some(ref header) = autocryptheader {
|
||||
let p = Peerstate::from_header(context, header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Possibly perform decryption
|
||||
/* possibly perform decryption */
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
|
||||
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||
let mut signatures = HashSet::default();
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
|
||||
peerstate = Peerstate::from_addr(&context, &from).await?;
|
||||
}
|
||||
if let Some(peerstate) = peerstate {
|
||||
peerstate.handle_fingerprint_change(context).await?;
|
||||
if let Some(key) = &peerstate.public_key {
|
||||
public_keyring_for_validate.add(key.clone());
|
||||
} else if let Some(key) = &peerstate.gossip_key {
|
||||
public_keyring_for_validate.add(key.clone());
|
||||
if let Some(key) = peerstate.public_key {
|
||||
public_keyring_for_validate.add(key);
|
||||
} else if let Some(key) = peerstate.gossip_key {
|
||||
public_keyring_for_validate.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,18 +181,6 @@ pub async fn try_decrypt(
|
||||
&mut signatures,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
// If message is not encrypted and it is not a read receipt, degrade encryption.
|
||||
if out_mail.is_none()
|
||||
&& message_time > peerstate.last_seen_autocrypt
|
||||
&& !contains_report(mail)
|
||||
{
|
||||
peerstate.degrade_encryption(message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((out_mail, signatures))
|
||||
}
|
||||
|
||||
@@ -321,11 +319,6 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::message::Message;
|
||||
use crate::param::Param;
|
||||
use crate::test_utils::*;
|
||||
|
||||
mod ensure_secret_key_exists {
|
||||
@@ -386,106 +379,4 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
let data = b"blas";
|
||||
assert_eq!(has_decrypted_pgp_armor(data), false);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_encrypted_no_autocrypt() -> crate::error::Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let (contact_alice_id, _modified) = Contact::add_or_lookup(
|
||||
&bob.ctx,
|
||||
"Alice",
|
||||
"alice@example.com",
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
||||
&alice.ctx,
|
||||
"Bob",
|
||||
"bob@example.net",
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chat_alice = chat::create_by_contact_id(&alice.ctx, contact_bob_id).await?;
|
||||
let chat_bob = chat::create_by_contact_id(&bob.ctx, contact_alice_id).await?;
|
||||
|
||||
// Alice sends unencrypted message to Bob
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives unencrypted message from Alice
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
|
||||
// Parsing a message is enough to update peerstate
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Bob sends encrypted message to Alice
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::prepare_msg(&bob.ctx, chat_bob, &mut msg).await?;
|
||||
chat::send_msg(&bob.ctx, chat_bob, &mut msg).await?;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
// Alice receives encrypted message from Bob
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
|
||||
let peerstate_bob = Peerstate::from_addr(&alice.ctx, "bob@example.net")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_bob.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Now Alice and Bob have established keys.
|
||||
|
||||
// Alice sends encrypted message without Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message with Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Alice sends plaintext message without Autocrypt header.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
|
||||
.await?
|
||||
.expect("no peerstate found in the database");
|
||||
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Reset);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,10 @@ pub enum EventType {
|
||||
#[strum(props(id = "105"))]
|
||||
ImapMessageMoved(String),
|
||||
|
||||
/// Emitted when an IMAP folder was emptied
|
||||
#[strum(props(id = "106"))]
|
||||
ImapFolderEmptied(String),
|
||||
|
||||
/// Emitted when an new file in the $BLOBDIR was created
|
||||
#[strum(props(id = "150"))]
|
||||
NewBlobFile(String),
|
||||
@@ -233,16 +237,10 @@ pub enum EventType {
|
||||
LocationChanged(Option<u32>),
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
///
|
||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
#[strum(props(id = "2041"))]
|
||||
ConfigureProgress {
|
||||
/// Progress.
|
||||
///
|
||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||
progress: usize,
|
||||
|
||||
/// Progress comment or error, something to display to the user.
|
||||
comment: Option<String>,
|
||||
},
|
||||
ConfigureProgress(usize),
|
||||
|
||||
/// Inform about the import/export progress started by imex().
|
||||
///
|
||||
|
||||
@@ -3,6 +3,7 @@ use mailparse::{MailHeader, MailHeaderMap};
|
||||
|
||||
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
#[allow(dead_code)]
|
||||
pub enum HeaderDef {
|
||||
MessageId,
|
||||
Subject,
|
||||
|
||||
@@ -25,7 +25,7 @@ impl Imap {
|
||||
if !self.can_idle() {
|
||||
bail!("IMAP server does not have IDLE capability");
|
||||
}
|
||||
self.setup_handle(context).await?;
|
||||
self.setup_handle_if_needed(context).await?;
|
||||
|
||||
self.select_folder(context, watch_folder.clone()).await?;
|
||||
|
||||
|
||||
101
src/imap/mod.rs
101
src/imap/mod.rs
@@ -68,6 +68,7 @@ const DELETE_CHECK_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
|
||||
const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
|
||||
const JUST_UID: &str = "(UID)";
|
||||
const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])";
|
||||
const SELECT_ALL: &str = "1:*";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Imap {
|
||||
@@ -161,11 +162,7 @@ impl Imap {
|
||||
self.should_reconnect = true;
|
||||
}
|
||||
|
||||
/// Connects or reconnects if needed.
|
||||
///
|
||||
/// It is safe to call this function if already connected, actions
|
||||
/// are performed only as needed.
|
||||
async fn try_setup_handle(&mut self, context: &Context) -> Result<()> {
|
||||
async fn setup_handle_if_needed(&mut self, context: &Context) -> Result<()> {
|
||||
if self.config.lp.server.is_empty() {
|
||||
bail!("IMAP operation attempted while it is torn down");
|
||||
}
|
||||
@@ -241,7 +238,9 @@ impl Imap {
|
||||
)
|
||||
.await
|
||||
};
|
||||
bail!("{}: {}", message, err);
|
||||
// IMAP connection failures are reported to users
|
||||
emit_event!(context, EventType::ErrorNetwork(message));
|
||||
bail!("IMAP connection failed: {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -263,6 +262,7 @@ impl Imap {
|
||||
.await;
|
||||
|
||||
warn!(context, "{} ({})", message, err);
|
||||
emit_event!(context, EventType::ErrorNetwork(message.clone()));
|
||||
|
||||
let lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if self.login_failed_once
|
||||
@@ -274,7 +274,7 @@ impl Imap {
|
||||
drop(lock);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(message.clone());
|
||||
msg.text = Some(message);
|
||||
if let Err(e) =
|
||||
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
|
||||
.await
|
||||
@@ -286,24 +286,11 @@ impl Imap {
|
||||
}
|
||||
|
||||
self.trigger_reconnect();
|
||||
Err(format_err!("{}: {}", message, err))
|
||||
Err(format_err!("IMAP Could not login as {}", imap_user))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects or reconnects if not already connected.
|
||||
///
|
||||
/// This function emits network error if it fails. It should not
|
||||
/// be used during configuration to avoid showing failed attempt
|
||||
/// errors to the user.
|
||||
async fn setup_handle(&mut self, context: &Context) -> Result<()> {
|
||||
let res = self.try_setup_handle(context).await;
|
||||
if let Err(ref err) = res {
|
||||
emit_event!(context, EventType::ErrorNetwork(err.to_string()));
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
async fn unsetup_handle(&mut self, context: &Context) {
|
||||
// Close folder if messages should be expunged
|
||||
if let Err(err) = self.close_folder(context).await {
|
||||
@@ -331,9 +318,7 @@ impl Imap {
|
||||
cfg.can_move = false;
|
||||
}
|
||||
|
||||
/// Connects to IMAP account using already-configured parameters.
|
||||
///
|
||||
/// Emits network error if connection fails.
|
||||
/// Connects to imap account using already-configured parameters.
|
||||
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
|
||||
if self.is_connected() && !self.should_reconnect() {
|
||||
return Ok(());
|
||||
@@ -363,9 +348,6 @@ impl Imap {
|
||||
/// Tries connecting to imap account using the specific login parameters.
|
||||
///
|
||||
/// `addr` is used to renew token if OAuth2 authentication is used.
|
||||
///
|
||||
/// Does not emit network errors, can be used to try various
|
||||
/// parameters during autoconfiguration.
|
||||
pub async fn connect(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -393,8 +375,8 @@ impl Imap {
|
||||
config.oauth2 = oauth2;
|
||||
}
|
||||
|
||||
if let Err(err) = self.try_setup_handle(context).await {
|
||||
warn!(context, "try_setup_handle: {}", err);
|
||||
if let Err(err) = self.setup_handle_if_needed(context).await {
|
||||
warn!(context, "failed to setup imap handle: {}", err);
|
||||
self.free_connect_params().await;
|
||||
return Err(err);
|
||||
}
|
||||
@@ -440,10 +422,7 @@ impl Imap {
|
||||
if teardown {
|
||||
self.disconnect(context).await;
|
||||
|
||||
warn!(
|
||||
context,
|
||||
"IMAP disconnected immediately after connecting due to error"
|
||||
);
|
||||
bail!("IMAP disconnected immediately after connecting due to error");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -458,7 +437,7 @@ impl Imap {
|
||||
// probably shutdown
|
||||
bail!("IMAP operation attempted while it is torn down");
|
||||
}
|
||||
self.setup_handle(context).await?;
|
||||
self.setup_handle_if_needed(context).await?;
|
||||
|
||||
while self.fetch_new_messages(context, &watch_folder).await? {
|
||||
// We fetch until no more new messages are there.
|
||||
@@ -1337,6 +1316,60 @@ impl Imap {
|
||||
info!(context, "FINISHED configuring IMAP-folders.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn empty_folder(&mut self, context: &Context, folder: &str) {
|
||||
info!(context, "emptying folder {}", folder);
|
||||
|
||||
// we want to report all error to the user
|
||||
// (no retry should be attempted)
|
||||
if folder.is_empty() {
|
||||
error!(context, "cannot perform empty, folder not set");
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.setup_handle_if_needed(context).await {
|
||||
error!(context, "could not setup imap connection: {:?}", err);
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.select_folder(context, Some(&folder)).await {
|
||||
error!(
|
||||
context,
|
||||
"Could not select {} for expunging: {:?}", folder, err
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if !self
|
||||
.add_flag_finalized_with_set(context, SELECT_ALL, "\\Deleted")
|
||||
.await
|
||||
{
|
||||
error!(context, "Cannot mark messages for deletion {}", folder);
|
||||
return;
|
||||
}
|
||||
|
||||
// we now trigger expunge to actually delete messages
|
||||
self.config.selected_folder_needs_expunge = true;
|
||||
match self.select_folder::<String>(context, None).await {
|
||||
Ok(()) => {
|
||||
emit_event!(context, EventType::ImapFolderEmptied(folder.to_string()));
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "expunge failed {}: {:?}", folder, err);
|
||||
}
|
||||
}
|
||||
if let Err(err) = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET server_folder='',server_uid=0 WHERE server_folder=?",
|
||||
paramsv![folder],
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"Failed to reset server_uid and server_folder for deleted messages: {}", err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST.
|
||||
|
||||
@@ -220,8 +220,10 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
msg.param
|
||||
.set(Param::MimeType, "application/autocrypt-setup");
|
||||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
msg.param.set_int(
|
||||
Param::ForcePlaintext,
|
||||
ForcePlaintext::NoAutocryptHeader as i32,
|
||||
);
|
||||
|
||||
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
info!(context, "Wait for setup message being sent ...",);
|
||||
|
||||
24
src/job.rs
24
src/job.rs
@@ -17,6 +17,7 @@ use async_smtp::smtp::response::Detail;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
@@ -92,6 +93,7 @@ pub enum Action {
|
||||
|
||||
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
|
||||
Housekeeping = 105, // low priority ...
|
||||
EmptyServer = 107,
|
||||
MarkseenMsgOnImap = 130,
|
||||
|
||||
// Moving message is prioritized lower than deletion so we don't
|
||||
@@ -126,6 +128,7 @@ impl From<Action> for Thread {
|
||||
Housekeeping => Thread::Imap,
|
||||
DeleteMsgOnImap => Thread::Imap,
|
||||
ResyncFolders => Thread::Imap,
|
||||
EmptyServer => Thread::Imap,
|
||||
MarkseenMsgOnImap => Thread::Imap,
|
||||
MoveMsg => Thread::Imap,
|
||||
|
||||
@@ -657,6 +660,25 @@ impl Job {
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
async fn empty_server(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
if self.foreign_id & DC_EMPTY_MVBOX > 0 {
|
||||
if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await {
|
||||
imap.empty_folder(context, &mvbox_folder).await;
|
||||
}
|
||||
}
|
||||
if self.foreign_id & DC_EMPTY_INBOX > 0 {
|
||||
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await {
|
||||
imap.empty_folder(context, &inbox_folder).await;
|
||||
}
|
||||
}
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
|
||||
async fn markseen_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.connect_configured(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
@@ -1003,6 +1025,7 @@ async fn perform_job_action(
|
||||
Action::MaybeSendLocationsEnded => {
|
||||
location::job_maybe_send_locations_ended(context, job).await
|
||||
}
|
||||
Action::EmptyServer => job.empty_server(context, connection.inbox()).await,
|
||||
Action::DeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
|
||||
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
|
||||
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
|
||||
@@ -1069,6 +1092,7 @@ pub async fn add(context: &Context, job: Job) {
|
||||
match action {
|
||||
Action::Unknown => unreachable!(),
|
||||
Action::Housekeeping
|
||||
| Action::EmptyServer
|
||||
| Action::DeleteMsgOnImap
|
||||
| Action::ResyncFolders
|
||||
| Action::MarkseenMsgOnImap
|
||||
|
||||
18
src/key.rs
18
src/key.rs
@@ -355,7 +355,7 @@ pub async fn store_self_keypair(
|
||||
}
|
||||
|
||||
/// A key fingerprint
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Fingerprint(Vec<u8>);
|
||||
|
||||
impl Fingerprint {
|
||||
@@ -375,14 +375,6 @@ impl Fingerprint {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Fingerprint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Fingerprint")
|
||||
.field("hex", &self.hex())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a human-readable fingerprint.
|
||||
impl fmt::Display for Fingerprint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -537,11 +529,11 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[test]
|
||||
fn test_from_slice_bad_data() {
|
||||
let mut bad_data: [u8; 4096] = [0; 4096];
|
||||
for (i, v) in bad_data.iter_mut().enumerate() {
|
||||
*v = (i & 0xff) as u8;
|
||||
for i in 0..4096 {
|
||||
bad_data[i] = (i & 0xff) as u8;
|
||||
}
|
||||
for j in 0..(4096 / 40) {
|
||||
let slice = &bad_data.get(j..j + 4096 / 2 + j).unwrap();
|
||||
let slice = &bad_data[j..j + 4096 / 2 + j];
|
||||
assert!(SignedPublicKey::from_slice(slice).is_err());
|
||||
assert!(SignedSecretKey::from_slice(slice).is_err());
|
||||
}
|
||||
@@ -601,7 +593,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
let ctx0 = ctx.clone();
|
||||
let thr0 =
|
||||
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx0)));
|
||||
let ctx1 = ctx;
|
||||
let ctx1 = ctx.clone();
|
||||
let thr1 =
|
||||
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx1)));
|
||||
let res0 = thr0.join().unwrap();
|
||||
|
||||
@@ -723,8 +723,6 @@ pub(crate) async fn job_maybe_send_locations_ended(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
@@ -774,7 +772,7 @@ mod tests {
|
||||
assert!(locations_ref[0].latitude < 51.423724f64);
|
||||
assert!(locations_ref[0].longitude >= 8.552556f64);
|
||||
assert!(locations_ref[0].longitude < 8.552557f64);
|
||||
assert!(locations_ref[0].accuracy.abs() < f64::EPSILON);
|
||||
assert_eq!(locations_ref[0].accuracy, 0.0f64);
|
||||
assert_eq!(locations_ref[0].timestamp, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,7 +494,7 @@ impl Message {
|
||||
pub fn get_text(&self) -> Option<String> {
|
||||
self.text
|
||||
.as_ref()
|
||||
.map(|text| dc_truncate(text, DC_MAX_GET_TEXT_LEN).to_string())
|
||||
.map(|text| dc_truncate(text, 30000).to_string())
|
||||
}
|
||||
|
||||
pub fn get_filename(&self) -> Option<String> {
|
||||
@@ -969,7 +969,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
|
||||
return ret;
|
||||
}
|
||||
let rawtxt = rawtxt.unwrap_or_default();
|
||||
let rawtxt = dc_truncate(rawtxt.trim(), DC_MAX_GET_INFO_LEN);
|
||||
let rawtxt = dc_truncate(rawtxt.trim(), 100_000);
|
||||
|
||||
let fts = dc_timestamp_to_str(msg.get_timestamp());
|
||||
ret += &format!("Sent: {}", fts);
|
||||
@@ -1797,6 +1797,16 @@ pub async fn update_server_uid(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn dc_empty_server(context: &Context, flags: u32) {
|
||||
job::kill_action(context, Action::EmptyServer).await;
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::EmptyServer, flags, Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1883,7 +1893,8 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(Viewtype::Voice, no_text.as_ref(), &some_file, 50, &ctx).await,
|
||||
get_summarytext_by_raw(Viewtype::Voice, no_text.as_ref(), &mut some_file, 50, &ctx)
|
||||
.await,
|
||||
"Voice message" // file names are not added for voice messages
|
||||
);
|
||||
|
||||
@@ -1893,7 +1904,8 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(Viewtype::Audio, no_text.as_ref(), &some_file, 50, &ctx).await,
|
||||
get_summarytext_by_raw(Viewtype::Audio, no_text.as_ref(), &mut some_file, 50, &ctx)
|
||||
.await,
|
||||
"Audio \u{2013} foo.bar" // file name is added for audio
|
||||
);
|
||||
|
||||
@@ -1909,7 +1921,8 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(Viewtype::File, some_text.as_ref(), &some_file, 50, &ctx).await,
|
||||
get_summarytext_by_raw(Viewtype::File, some_text.as_ref(), &mut some_file, 50, &ctx)
|
||||
.await,
|
||||
"File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files
|
||||
);
|
||||
|
||||
@@ -1917,7 +1930,7 @@ mod tests {
|
||||
asm_file.set(Param::File, "foo.bar");
|
||||
asm_file.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), &asm_file, 50, &ctx).await,
|
||||
get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), &mut asm_file, 50, &ctx).await,
|
||||
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
|
||||
);
|
||||
}
|
||||
@@ -1994,7 +2007,7 @@ mod tests {
|
||||
let chatitems = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
|
||||
for chatitem in chatitems {
|
||||
if let ChatItem::Message { msg_id } = chatitem {
|
||||
if let Ok(msg) = Message::load_from_db(&t.ctx, msg_id).await {
|
||||
if let Ok(msg) = Message::load_from_db(&t.ctx, msg_id.clone()).await {
|
||||
if msg.get_viewtype() == Viewtype::Image {
|
||||
has_image = true;
|
||||
// just check that width/height are inside some reasonable ranges
|
||||
|
||||
@@ -237,16 +237,22 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
return true;
|
||||
}
|
||||
|
||||
!self
|
||||
let force_plaintext = self
|
||||
.msg
|
||||
.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default()
|
||||
&& self
|
||||
.get_int(Param::ForcePlaintext)
|
||||
.unwrap_or_default();
|
||||
|
||||
if force_plaintext == 0 {
|
||||
return self
|
||||
.msg
|
||||
.param
|
||||
.get_bool(Param::GuaranteeE2ee)
|
||||
.get_int(Param::GuaranteeE2ee)
|
||||
.unwrap_or_default()
|
||||
!= 0;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
Loaded::MDN { .. } => false,
|
||||
}
|
||||
@@ -265,30 +271,19 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_force_plaintext(&self) -> bool {
|
||||
fn should_force_plaintext(&self) -> i32 {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat } => {
|
||||
if chat.typ == Chattype::VerifiedGroup {
|
||||
false
|
||||
0
|
||||
} else {
|
||||
self.msg
|
||||
.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.get_int(Param::ForcePlaintext)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
Loaded::MDN { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_skip_autocrypt(&self) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { .. } => self
|
||||
.msg
|
||||
.param
|
||||
.get_bool(Param::SkipAutocrypt)
|
||||
.unwrap_or_default(),
|
||||
Loaded::MDN { .. } => true,
|
||||
Loaded::MDN { .. } => ForcePlaintext::NoAutocryptHeader as i32,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,7 +476,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let min_verified = self.min_verified();
|
||||
let grpimage = self.grpimage();
|
||||
let force_plaintext = self.should_force_plaintext();
|
||||
let skip_autocrypt = self.should_skip_autocrypt();
|
||||
let subject_str = self.subject_str().await;
|
||||
let e2ee_guaranteed = self.is_e2ee_guaranteed();
|
||||
let encrypt_helper = EncryptHelper::new(self.context).await?;
|
||||
@@ -505,7 +499,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
Loaded::MDN { .. } => self.render_mdn().await?,
|
||||
};
|
||||
|
||||
if !skip_autocrypt {
|
||||
if force_plaintext != ForcePlaintext::NoAutocryptHeader as i32 {
|
||||
// unless determined otherwise we add the Autocrypt header
|
||||
let aheader = encrypt_helper.get_aheader().to_string();
|
||||
unprotected_headers.push(Header::new("Autocrypt".into(), aheader));
|
||||
@@ -516,7 +510,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let peerstates = self.peerstates_for_recipients().await?;
|
||||
let should_encrypt =
|
||||
encrypt_helper.should_encrypt(self.context, e2ee_guaranteed, &peerstates)?;
|
||||
let is_encrypted = should_encrypt && !force_plaintext;
|
||||
let is_encrypted = should_encrypt && force_plaintext == 0;
|
||||
|
||||
let rfc724_mid = match self.loaded {
|
||||
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
|
||||
|
||||
@@ -1318,8 +1318,6 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
|
||||
|
||||
18
src/param.rs
18
src/param.rs
@@ -40,12 +40,10 @@ pub enum Param {
|
||||
/// 'c' nor 'e' are preset, the messages is only transport encrypted.
|
||||
ErroneousE2ee = b'e',
|
||||
|
||||
/// For Messages: force unencrypted message, a value from `ForcePlaintext` enum.
|
||||
/// For Messages: force unencrypted message, either `ForcePlaintext::AddAutocryptHeader` (1),
|
||||
/// `ForcePlaintext::NoAutocryptHeader` (2) or 0.
|
||||
ForcePlaintext = b'u',
|
||||
|
||||
/// For Messages: do not include Autocrypt header.
|
||||
SkipAutocrypt = b'o',
|
||||
|
||||
/// For Messages
|
||||
WantsMdn = b'r',
|
||||
|
||||
@@ -130,6 +128,14 @@ pub enum Param {
|
||||
MsgId = b'I',
|
||||
}
|
||||
|
||||
/// Possible values for `Param::ForcePlaintext`.
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum ForcePlaintext {
|
||||
AddAutocryptHeader = 1,
|
||||
NoAutocryptHeader = 2,
|
||||
}
|
||||
|
||||
/// An object for handling key=value parameter lists.
|
||||
///
|
||||
/// The structure is serialized by calling `to_string()` on it.
|
||||
@@ -469,8 +475,8 @@ mod tests {
|
||||
);
|
||||
|
||||
// Blob in blobdir, expect blob.
|
||||
let bar_path = t.ctx.get_blobdir().join("bar");
|
||||
p.set(Param::File, bar_path.to_str().unwrap());
|
||||
let bar = t.ctx.get_blobdir().join("bar");
|
||||
p.set(Param::File, bar.to_str().unwrap());
|
||||
let blob = p
|
||||
.get_blob(Param::File, &t.ctx, false)
|
||||
.await
|
||||
|
||||
172
src/peerstate.rs
172
src/peerstate.rs
@@ -94,44 +94,49 @@ pub enum ToSave {
|
||||
}
|
||||
|
||||
impl<'a> Peerstate<'a> {
|
||||
pub fn from_header(context: &'a Context, header: &Aheader, message_time: i64) -> Self {
|
||||
pub fn new(context: &'a Context, addr: String) -> Self {
|
||||
Peerstate {
|
||||
context,
|
||||
addr: header.addr.clone(),
|
||||
last_seen: message_time,
|
||||
last_seen_autocrypt: message_time,
|
||||
prefer_encrypt: header.prefer_encrypt,
|
||||
public_key: Some(header.public_key.clone()),
|
||||
public_key_fingerprint: Some(header.public_key.fingerprint()),
|
||||
gossip_key: None,
|
||||
gossip_key_fingerprint: None,
|
||||
gossip_timestamp: 0,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_gossip(context: &'a Context, gossip_header: &Aheader, message_time: i64) -> Self {
|
||||
Peerstate {
|
||||
context,
|
||||
addr: gossip_header.addr.clone(),
|
||||
addr,
|
||||
last_seen: 0,
|
||||
last_seen_autocrypt: 0,
|
||||
prefer_encrypt: Default::default(),
|
||||
public_key: None,
|
||||
public_key_fingerprint: None,
|
||||
gossip_key: Some(gossip_header.public_key.clone()),
|
||||
gossip_key_fingerprint: Some(gossip_header.public_key.fingerprint()),
|
||||
gossip_timestamp: message_time,
|
||||
gossip_key: None,
|
||||
gossip_key_fingerprint: None,
|
||||
gossip_timestamp: 0,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
to_save: None,
|
||||
fingerprint_changed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_header(context: &'a Context, header: &Aheader, message_time: i64) -> Self {
|
||||
let mut res = Self::new(context, header.addr.clone());
|
||||
|
||||
res.last_seen = message_time;
|
||||
res.last_seen_autocrypt = message_time;
|
||||
res.to_save = Some(ToSave::All);
|
||||
res.prefer_encrypt = header.prefer_encrypt;
|
||||
res.public_key = Some(header.public_key.clone());
|
||||
res.recalc_fingerprint();
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub fn from_gossip(context: &'a Context, gossip_header: &Aheader, message_time: i64) -> Self {
|
||||
let mut res = Self::new(context, gossip_header.addr.clone());
|
||||
|
||||
res.gossip_timestamp = message_time;
|
||||
res.to_save = Some(ToSave::All);
|
||||
res.gossip_key = Some(gossip_header.public_key.clone());
|
||||
res.recalc_fingerprint();
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn from_addr(context: &'a Context, addr: &str) -> Result<Option<Peerstate<'a>>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||
@@ -170,44 +175,40 @@ impl<'a> Peerstate<'a> {
|
||||
public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
|
||||
gossip_key_fingerprint, verified_key, verified_key_fingerprint
|
||||
*/
|
||||
let mut res = Self::new(context, row.get(0)?);
|
||||
|
||||
let res = Peerstate {
|
||||
context,
|
||||
addr: row.get(0)?,
|
||||
last_seen: row.get(1)?,
|
||||
last_seen_autocrypt: row.get(2)?,
|
||||
prefer_encrypt: EncryptPreference::from_i32(row.get(3)?).unwrap_or_default(),
|
||||
public_key: row
|
||||
.get(4)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
|
||||
public_key_fingerprint: row
|
||||
.get::<_, Option<String>>(7)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
gossip_key: row
|
||||
.get(6)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
|
||||
gossip_key_fingerprint: row
|
||||
.get::<_, Option<String>>(8)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
gossip_timestamp: row.get(5)?,
|
||||
verified_key: row
|
||||
.get(9)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
|
||||
verified_key_fingerprint: row
|
||||
.get::<_, Option<String>>(10)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
to_save: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
res.last_seen = row.get(1)?;
|
||||
res.last_seen_autocrypt = row.get(2)?;
|
||||
res.prefer_encrypt = EncryptPreference::from_i32(row.get(3)?).unwrap_or_default();
|
||||
res.gossip_timestamp = row.get(5)?;
|
||||
|
||||
res.public_key_fingerprint = row
|
||||
.get::<_, Option<String>>(7)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default();
|
||||
res.gossip_key_fingerprint = row
|
||||
.get::<_, Option<String>>(8)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default();
|
||||
res.verified_key_fingerprint = row
|
||||
.get::<_, Option<String>>(10)?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default();
|
||||
res.public_key = row
|
||||
.get(4)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
res.gossip_key = row
|
||||
.get(6)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
res.verified_key = row
|
||||
.get(9)
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
@@ -290,7 +291,7 @@ impl<'a> Peerstate<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
if message_time > self.last_seen {
|
||||
if message_time > self.last_seen_autocrypt {
|
||||
self.last_seen = message_time;
|
||||
self.last_seen_autocrypt = message_time;
|
||||
self.to_save = Some(ToSave::Timestamps);
|
||||
@@ -489,6 +490,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_peerstate_save_to_db() {
|
||||
@@ -635,44 +637,10 @@ mod tests {
|
||||
assert_eq!(peerstate.verified_key_fingerprint, None);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_peerstate_degrade_reordering() {
|
||||
let context = crate::test_utils::TestContext::new().await.ctx;
|
||||
let addr = "example@example.org";
|
||||
let pub_key = alice_keypair().public;
|
||||
let header = Aheader::new(addr.to_string(), pub_key, EncryptPreference::Mutual);
|
||||
|
||||
let mut peerstate = Peerstate {
|
||||
context: &context,
|
||||
addr: addr.to_string(),
|
||||
last_seen: 0,
|
||||
last_seen_autocrypt: 0,
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
public_key: None,
|
||||
public_key_fingerprint: None,
|
||||
gossip_key: None,
|
||||
gossip_timestamp: 0,
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::NoPreference);
|
||||
|
||||
peerstate.apply_header(&header, 100);
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
peerstate.degrade_encryption(300);
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
|
||||
|
||||
// This has message time 200, while encryption was degraded at timestamp 300.
|
||||
// Because of reordering, header should not be applied.
|
||||
peerstate.apply_header(&header, 200);
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
|
||||
|
||||
// Same header will be applied in the future.
|
||||
peerstate.apply_header(&header, 400);
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
// TODO: don't copy this from stress.rs
|
||||
#[allow(dead_code)]
|
||||
struct TestContext {
|
||||
ctx: Context,
|
||||
dir: TempDir,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,9 +439,9 @@ mod tests {
|
||||
let bob = bob_keypair();
|
||||
TestKeys {
|
||||
alice_secret: alice.secret.clone(),
|
||||
alice_public: alice.public,
|
||||
alice_public: alice.public.clone(),
|
||||
bob_secret: bob.secret.clone(),
|
||||
bob_public: bob.public,
|
||||
bob_public: bob.public.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +93,6 @@ pub fn get_provider_info(addr: &str) -> Option<&Provider> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol)
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
@@ -12,6 +11,7 @@ use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::*;
|
||||
use crate::error::{bail, Error};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
||||
@@ -21,7 +21,6 @@ use crate::mimeparser::*;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::qr::check_qr;
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::token;
|
||||
|
||||
@@ -68,6 +67,18 @@ macro_rules! get_qr_attr {
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum BobStatus {
|
||||
Error,
|
||||
Success,
|
||||
}
|
||||
|
||||
impl Default for BobStatus {
|
||||
fn default() -> Self {
|
||||
Self::Error
|
||||
}
|
||||
}
|
||||
|
||||
/// State for setup-contact/secure-join protocol joiner's side.
|
||||
///
|
||||
/// The setup-contact protocol needs to carry state for both the inviter (Alice) and the
|
||||
@@ -77,6 +88,11 @@ macro_rules! get_qr_attr {
|
||||
pub(crate) struct Bob {
|
||||
/// The next message expected by the protocol.
|
||||
expects: SecureJoinStep,
|
||||
/// The final status of the last-run setup-contact/secure-join protocol.
|
||||
///
|
||||
/// This is only meaningful if you know you have exited the protocol but have not
|
||||
/// started a new one.
|
||||
pub status: BobStatus,
|
||||
/// The QR-scanned information of the currently running protocol.
|
||||
pub qr_scan: Option<Lot>,
|
||||
}
|
||||
@@ -188,79 +204,78 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup(context: &Context, ongoing_allocated: bool) {
|
||||
async fn cleanup(
|
||||
context: &Context,
|
||||
contact_chat_id: ChatId,
|
||||
ongoing_allocated: bool,
|
||||
join_vg: bool,
|
||||
) -> ChatId {
|
||||
let mut bob = context.bob.write().await;
|
||||
bob.expects = SecureJoinStep::NotActive;
|
||||
let ret_chat_id: ChatId = if bob.status == BobStatus::Success {
|
||||
if join_vg {
|
||||
chat::get_chat_id_by_grpid(
|
||||
context,
|
||||
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or((ChatId::new(0), false, Blocked::Not))
|
||||
.0
|
||||
} else {
|
||||
contact_chat_id
|
||||
}
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
};
|
||||
bob.qr_scan = None;
|
||||
|
||||
if ongoing_allocated {
|
||||
context.free_ongoing().await;
|
||||
}
|
||||
ret_chat_id
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum JoinError {
|
||||
#[error("Unknown QR-code")]
|
||||
QrCode,
|
||||
#[error("Aborted by user")]
|
||||
Aborted,
|
||||
#[error("Failed to send handshake message")]
|
||||
SendMessage(#[from] SendMsgError),
|
||||
// Note that this can currently only occur if there is a bug in the QR/Lot code as this
|
||||
// is supposed to create a contact for us.
|
||||
#[error("Unknown contact (this is a bug)")]
|
||||
UnknownContact,
|
||||
// Note that this can only occur if we failed to create the chat correctly.
|
||||
#[error("No Chat found for group (this is a bug)")]
|
||||
MissingChat(#[source] sql::Error),
|
||||
}
|
||||
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
|
||||
///
|
||||
/// This is the start of the process for the joiner. See the module and ffi documentation
|
||||
/// for more details.
|
||||
///
|
||||
/// 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> {
|
||||
/// Take a scanned QR-code and do the setup-contact/join-group handshake.
|
||||
/// See the ffi-documentation for more details.
|
||||
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
if context.alloc_ongoing().await.is_err() {
|
||||
cleanup(&context, false).await;
|
||||
return Err(JoinError::Aborted);
|
||||
return cleanup(&context, ChatId::new(0), false, false).await;
|
||||
}
|
||||
|
||||
securejoin(context, qr).await
|
||||
}
|
||||
|
||||
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
async fn securejoin(context: &Context, qr: &str) -> ChatId {
|
||||
/*========================================================
|
||||
==== Bob - the joiner's side =====
|
||||
==== Step 2 in "Setup verified contact" protocol =====
|
||||
========================================================*/
|
||||
|
||||
let mut contact_chat_id = ChatId::new(0);
|
||||
let mut join_vg: bool = false;
|
||||
|
||||
info!(context, "Requesting secure-join ...",);
|
||||
ensure_secret_key_exists(context).await.ok();
|
||||
let qr_scan = check_qr(context, &qr).await;
|
||||
if qr_scan.state != LotState::QrAskVerifyContact && qr_scan.state != LotState::QrAskVerifyGroup
|
||||
{
|
||||
error!(context, "Unknown QR code.",);
|
||||
cleanup(&context, true).await;
|
||||
return Err(JoinError::QrCode);
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
let contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id).await {
|
||||
contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id).await {
|
||||
Ok(chat_id) => chat_id,
|
||||
Err(_) => {
|
||||
error!(context, "Unknown contact.");
|
||||
cleanup(&context, true).await;
|
||||
return Err(JoinError::UnknownContact);
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
};
|
||||
if context.shall_stop_ongoing().await {
|
||||
cleanup(&context, true).await;
|
||||
return Err(JoinError::Aborted);
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
let join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
|
||||
join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
|
||||
{
|
||||
let mut bob = context.bob.write().await;
|
||||
bob.status = BobStatus::Error;
|
||||
bob.qr_scan = Some(qr_scan);
|
||||
}
|
||||
if fingerprint_equals_sender(
|
||||
@@ -310,8 +325,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
.await
|
||||
{
|
||||
error!(context, "failed to send handshake message: {}", err);
|
||||
cleanup(&context, true).await;
|
||||
return Err(JoinError::SendMessage(err));
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
} else {
|
||||
context.bob.write().await.expects = SecureJoinStep::AuthRequired;
|
||||
@@ -328,8 +342,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
.await
|
||||
{
|
||||
error!(context, "failed to send handshake message: {}", err);
|
||||
cleanup(&context, true).await;
|
||||
return Err(JoinError::SendMessage(err));
|
||||
return cleanup(&context, contact_chat_id, true, join_vg).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,45 +351,15 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
|
||||
while !context.shall_stop_ongoing().await {
|
||||
async_std::task::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
|
||||
// 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.
|
||||
let start = Instant::now();
|
||||
let chatid = loop {
|
||||
{
|
||||
let bob = context.bob.read().await;
|
||||
let grpid = bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap();
|
||||
match chat::get_chat_id_by_grpid(context, grpid).await {
|
||||
Ok((chatid, _is_verified, _blocked)) => break chatid,
|
||||
Err(err) => {
|
||||
if start.elapsed() > Duration::from_secs(7) {
|
||||
return Err(JoinError::MissingChat(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async_std::task::sleep(Duration::from_millis(50)).await;
|
||||
};
|
||||
|
||||
cleanup(&context, true).await;
|
||||
Ok(chatid)
|
||||
cleanup(&context, contact_chat_id, true, join_vg).await
|
||||
} else {
|
||||
// for a one-to-one-chat, the chat is already known, return the chat-id,
|
||||
// the verification runs in background
|
||||
context.free_ongoing().await;
|
||||
Ok(contact_chat_id)
|
||||
contact_chat_id
|
||||
}
|
||||
}
|
||||
|
||||
/// Error for [send_handshake_msg].
|
||||
///
|
||||
/// Wrapping the [anyhow::Error] means we can "impl From" more easily on errors from this
|
||||
/// function.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Failed sending handshake message")]
|
||||
pub struct SendMsgError(#[from] anyhow::Error);
|
||||
|
||||
async fn send_handshake_msg(
|
||||
context: &Context,
|
||||
contact_chat_id: ChatId,
|
||||
@@ -384,7 +367,7 @@ async fn send_handshake_msg(
|
||||
param2: impl AsRef<str>,
|
||||
fingerprint: Option<Fingerprint>,
|
||||
grpid: impl AsRef<str>,
|
||||
) -> Result<(), SendMsgError> {
|
||||
) -> Result<(), HandshakeError> {
|
||||
let mut msg = Message::default();
|
||||
msg.viewtype = Viewtype::Text;
|
||||
msg.text = Some(format!("Secure-Join: {}", step));
|
||||
@@ -405,12 +388,18 @@ async fn send_handshake_msg(
|
||||
msg.param.set(Param::Arg4, grpid.as_ref());
|
||||
}
|
||||
if step == "vg-request" || step == "vc-request" {
|
||||
msg.param.set_int(Param::ForcePlaintext, 1);
|
||||
msg.param.set_int(
|
||||
Param::ForcePlaintext,
|
||||
ForcePlaintext::AddAutocryptHeader as i32,
|
||||
);
|
||||
} else {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
}
|
||||
|
||||
chat::send_msg(context, contact_chat_id, &mut msg).await?;
|
||||
chat::send_msg(context, contact_chat_id, &mut msg)
|
||||
.await
|
||||
.map_err(HandshakeError::MsgSendFailed)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -470,7 +459,7 @@ pub(crate) enum HandshakeError {
|
||||
#[error("No configured self address found")]
|
||||
NoSelfAddr,
|
||||
#[error("Failed to send message")]
|
||||
MsgSendFailed(#[from] SendMsgError),
|
||||
MsgSendFailed(#[source] Error),
|
||||
#[error("Failed to parse fingerprint")]
|
||||
BadFingerprint(#[from] crate::key::FingerprintError),
|
||||
}
|
||||
@@ -608,6 +597,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
},
|
||||
)
|
||||
.await;
|
||||
context.bob.write().await.status = BobStatus::Error; // secure-join failed
|
||||
context.stop_ongoing().await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
@@ -620,6 +610,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
"Fingerprint mismatch on joiner-side.",
|
||||
)
|
||||
.await;
|
||||
context.bob.write().await.status = BobStatus::Error; // secure-join failed
|
||||
context.stop_ongoing().await;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
@@ -817,6 +808,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
"Contact confirm message not encrypted.",
|
||||
)
|
||||
.await;
|
||||
context.bob.write().await.status = BobStatus::Error;
|
||||
return Ok(abort_retval);
|
||||
}
|
||||
|
||||
@@ -865,6 +857,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.bob.write().await.status = BobStatus::Success;
|
||||
context.stop_ongoing().await;
|
||||
Ok(if join_vg {
|
||||
HandshakeMessage::Propagate
|
||||
@@ -884,7 +877,6 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if join_vg {
|
||||
// Responsible for showing "$Bob securely joined $group" message
|
||||
inviter_progress!(context, contact_id, 800);
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
let field_grpid = mime_message
|
||||
@@ -1096,337 +1088,3 @@ fn encrypted_and_signed(
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact() {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = dc_get_securejoin_qr(&alice.ctx, ChatId::new(0))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Bob scans QR-code, sends vc-request
|
||||
let bob_chatid = dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert_eq!(sent.id(), bob_chatid);
|
||||
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-request");
|
||||
assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
|
||||
// Alice receives vc-request, sends vc-auth-required
|
||||
alice.recv_msg(&sent).await;
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-auth-required");
|
||||
|
||||
// Bob receives vc-auth-required, sends vc-request-with-auth
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob_id =
|
||||
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown).await;
|
||||
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&alice.ctx).await,
|
||||
VerifiedStatus::Unverified
|
||||
);
|
||||
|
||||
// Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&alice.ctx).await,
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-contact-confirm"
|
||||
);
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown).await;
|
||||
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&bob.ctx).await,
|
||||
VerifiedStatus::Unverified
|
||||
);
|
||||
|
||||
// Bob receives vc-contact-confirm, sends vc-contact-confirm-received
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
contact_alice.is_verified(&bob.ctx).await,
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-contact-confirm-received"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact_bad_qr() {
|
||||
let bob = TestContext::new_bob().await;
|
||||
let ret = dc_join_securejoin(&bob.ctx, "not a qr code").await;
|
||||
assert!(matches!(ret, Err(JoinError::QrCode)));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_setup_contact_bob_knows_alice() {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Ensure Bob knows Alice_FP
|
||||
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await.unwrap();
|
||||
let peerstate = Peerstate {
|
||||
context: &bob.ctx,
|
||||
addr: "alice@example.com".into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 10,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(alice_pubkey.clone()),
|
||||
public_key_fingerprint: Some(alice_pubkey.fingerprint()),
|
||||
gossip_key: Some(alice_pubkey.clone()),
|
||||
gossip_timestamp: 10,
|
||||
gossip_key_fingerprint: Some(alice_pubkey.fingerprint()),
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
to_save: Some(ToSave::All),
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
peerstate.save_to_db(&bob.ctx.sql, true).await.unwrap();
|
||||
|
||||
// Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = dc_get_securejoin_qr(&alice.ctx, ChatId::new(0))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Bob scans QR-code, sends vc-request-with-auth, skipping vc-request
|
||||
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
||||
&alice.ctx,
|
||||
"Bob",
|
||||
"bob@example.net",
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&alice.ctx).await,
|
||||
VerifiedStatus::Unverified
|
||||
);
|
||||
|
||||
// Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&alice.ctx).await,
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-contact-confirm"
|
||||
);
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown).await;
|
||||
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&bob.ctx).await,
|
||||
VerifiedStatus::Unverified
|
||||
);
|
||||
|
||||
// Bob receives vc-contact-confirm, sends vc-contact-confirm-received
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
contact_alice.is_verified(&bob.ctx).await,
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-contact-confirm-received"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_secure_join() {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chatid = chat::create_group_chat(&alice.ctx, VerifiedStatus::Verified, "the chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Generate QR-code, secure-join implied by chatid
|
||||
let qr = dc_get_securejoin_qr(&alice.ctx, chatid).await.unwrap();
|
||||
|
||||
// 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_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-request");
|
||||
assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
|
||||
// Alice receives vg-request, sends vg-auth-required
|
||||
alice.recv_msg(&sent).await;
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-auth-required");
|
||||
|
||||
// Bob receives vg-auth-required, sends vg-request-with-auth
|
||||
bob.recv_msg(&sent).await;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get(HeaderDef::SecureJoin).unwrap(),
|
||||
"vg-request-with-auth"
|
||||
);
|
||||
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob_id =
|
||||
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown).await;
|
||||
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&alice.ctx).await,
|
||||
VerifiedStatus::Unverified
|
||||
);
|
||||
|
||||
// Alice receives vg-request-with-auth, sends vg-member-added
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&alice.ctx).await,
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-member-added");
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown).await;
|
||||
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
contact_bob.is_verified(&bob.ctx).await,
|
||||
VerifiedStatus::Unverified
|
||||
);
|
||||
|
||||
// Bob receives vg-member-added, sends vg-member-added-received
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
contact_alice.is_verified(&bob.ctx).await,
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get(HeaderDef::SecureJoin).unwrap(),
|
||||
"vg-member-added-received"
|
||||
);
|
||||
|
||||
let bob_chatid = joiner.await;
|
||||
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await.unwrap();
|
||||
assert!(bob_chat.is_verified());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,26 +101,13 @@ impl Smtp {
|
||||
}
|
||||
|
||||
let lp = LoginParam::from_database(context, "configured_").await;
|
||||
let res = self
|
||||
.connect(
|
||||
context,
|
||||
&lp.smtp,
|
||||
&lp.addr,
|
||||
lp.server_flags & DC_LP_AUTH_OAUTH2 != 0,
|
||||
)
|
||||
.await;
|
||||
if let Err(ref err) = res {
|
||||
let message = context
|
||||
.stock_string_repl_str2(
|
||||
StockMessage::ServerResponse,
|
||||
format!("SMTP {}:{}", lp.smtp.server, lp.smtp.port),
|
||||
err.to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
context.emit_event(EventType::ErrorNetwork(message));
|
||||
};
|
||||
res
|
||||
self.connect(
|
||||
context,
|
||||
&lp.smtp,
|
||||
&lp.addr,
|
||||
lp.server_flags & DC_LP_AUTH_OAUTH2 != 0,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Connect using the provided login params.
|
||||
@@ -137,6 +124,7 @@ impl Smtp {
|
||||
}
|
||||
|
||||
if lp.server.is_empty() || lp.port == 0 {
|
||||
context.emit_event(EventType::ErrorNetwork("SMTP bad parameters.".into()));
|
||||
return Err(Error::BadParameters);
|
||||
}
|
||||
|
||||
@@ -209,6 +197,15 @@ impl Smtp {
|
||||
|
||||
let mut trans = client.into_transport();
|
||||
if let Err(err) = trans.connect().await {
|
||||
let message = context
|
||||
.stock_string_repl_str2(
|
||||
StockMessage::ServerResponse,
|
||||
format!("SMTP {}:{}", domain, port),
|
||||
err.to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
emit_event!(context, EventType::ErrorNetwork(message));
|
||||
return Err(Error::ConnectionFailure(err));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ use async_smtp::*;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use itertools::Itertools;
|
||||
use std::time::Duration;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -34,7 +33,11 @@ impl Smtp {
|
||||
) -> Result<()> {
|
||||
let message_len_bytes = message.len();
|
||||
|
||||
let recipients_display = recipients.iter().map(|x| x.to_string()).join(",");
|
||||
let recipients_display = recipients
|
||||
.iter()
|
||||
.map(|x| format!("{}", x))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
|
||||
let envelope =
|
||||
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
|
||||
|
||||
@@ -216,9 +216,6 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "You are invited to a video chat, click %1$s to join."))]
|
||||
VideochatInviteMsgBody = 83,
|
||||
|
||||
#[strum(props(fallback = "Configuration failed. Error: “%1$s”"))]
|
||||
ConfigurationFailed = 84,
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -2,33 +2,20 @@
|
||||
//!
|
||||
//! This module is only compiled for test runs.
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::sync::RwLock;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::dc_tools::EmailAddress;
|
||||
use crate::job::Action;
|
||||
use crate::key::{self, DcKey};
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::param::{Param, Params};
|
||||
|
||||
/// A Context and temporary directory.
|
||||
///
|
||||
/// The temporary directory can be used to store the SQLite database,
|
||||
/// see e.g. [test_context] which does this.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TestContext {
|
||||
pub ctx: Context,
|
||||
pub dir: TempDir,
|
||||
/// Counter for fake IMAP UIDs in [recv_msg], for private use in that function only.
|
||||
recv_idx: RwLock<u32>,
|
||||
}
|
||||
|
||||
impl TestContext {
|
||||
@@ -48,11 +35,7 @@ impl TestContext {
|
||||
let ctx = Context::new("FakeOS".into(), dbfile.into(), id)
|
||||
.await
|
||||
.unwrap();
|
||||
Self {
|
||||
ctx,
|
||||
dir,
|
||||
recv_idx: RwLock::new(0),
|
||||
}
|
||||
Self { ctx, dir }
|
||||
}
|
||||
|
||||
/// Create a new configured [TestContext].
|
||||
@@ -65,19 +48,6 @@ impl TestContext {
|
||||
t
|
||||
}
|
||||
|
||||
/// Create a new configured [TestContext].
|
||||
///
|
||||
/// This is a shortcut which configures bob@example.net with a fixed key.
|
||||
pub async fn new_bob() -> Self {
|
||||
let t = Self::new().await;
|
||||
let keypair = bob_keypair();
|
||||
t.configure_addr(&keypair.addr.to_string()).await;
|
||||
key::store_self_keypair(&t.ctx, &keypair, key::KeyPairUse::Default)
|
||||
.await
|
||||
.expect("Failed to save Bob's key");
|
||||
t
|
||||
}
|
||||
|
||||
/// Configure with alice@example.com.
|
||||
///
|
||||
/// The context will be fake-configured as the alice user, with a pre-generated secret
|
||||
@@ -106,122 +76,6 @@ impl TestContext {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Retrieve a sent message from the jobs table.
|
||||
///
|
||||
/// This retrieves and removes a message which has been scheduled to send from the jobs
|
||||
/// table. Messages are returned in the order they have been sent.
|
||||
///
|
||||
/// Panics if there is no message or on any error.
|
||||
pub async fn pop_sent_msg(&self) -> SentMessage {
|
||||
let start = Instant::now();
|
||||
let (rowid, foreign_id, raw_params) = loop {
|
||||
let row = self
|
||||
.ctx
|
||||
.sql
|
||||
.query_row(
|
||||
r#"
|
||||
SELECT id, foreign_id, param
|
||||
FROM jobs
|
||||
WHERE action=?
|
||||
ORDER BY desired_timestamp;
|
||||
"#,
|
||||
paramsv![Action::SendMsgToSmtp],
|
||||
|row| {
|
||||
let id: i64 = row.get(0)?;
|
||||
let foreign_id: i64 = row.get(1)?;
|
||||
let param: String = row.get(2)?;
|
||||
Ok((id, foreign_id, param))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if let Ok(row) = row {
|
||||
break row;
|
||||
}
|
||||
if start.elapsed() < Duration::from_secs(3) {
|
||||
async_std::task::sleep(Duration::from_millis(100)).await;
|
||||
} else {
|
||||
panic!("no sent message found in jobs table");
|
||||
}
|
||||
};
|
||||
let id = ChatId::new(foreign_id as u32);
|
||||
let params = Params::from_str(&raw_params).unwrap();
|
||||
let blob_path = params
|
||||
.get_blob(Param::File, &self.ctx, false)
|
||||
.await
|
||||
.expect("failed to parse blob from param")
|
||||
.expect("no Param::File found in Params")
|
||||
.to_abs_path();
|
||||
self.ctx
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
|
||||
.await
|
||||
.expect("failed to remove job");
|
||||
SentMessage {
|
||||
id,
|
||||
params,
|
||||
blob_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a message.
|
||||
///
|
||||
/// Parsing a message does not run the entire receive pipeline, but is not without
|
||||
/// side-effects either. E.g. if the message includes autocrypt headers the relevant
|
||||
/// peerstates will be updated. Later receiving the message using [recv_msg] is
|
||||
/// unlikely to be affected as the peerstate would be processed again in exactly the
|
||||
/// same way.
|
||||
pub async fn parse_msg(&self, msg: &SentMessage) -> MimeMessage {
|
||||
MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Receive a message.
|
||||
///
|
||||
/// Receives a message using the `dc_receive_imf()` pipeline.
|
||||
pub async fn recv_msg(&self, msg: &SentMessage) {
|
||||
let mut idx = self.recv_idx.write().await;
|
||||
*idx += 1;
|
||||
dc_receive_imf(&self.ctx, msg.payload().as_bytes(), "INBOX", *idx, false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// A raw message as it was scheduled to be sent.
|
||||
///
|
||||
/// This is a raw message, probably in the shape DC was planning to send it but not having
|
||||
/// passed through a SMTP-IMAP pipeline.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SentMessage {
|
||||
id: ChatId,
|
||||
params: Params,
|
||||
blob_path: PathBuf,
|
||||
}
|
||||
|
||||
impl SentMessage {
|
||||
/// The ChatId the message belonged to.
|
||||
pub fn id(&self) -> ChatId {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// A recipient the message was destined for.
|
||||
///
|
||||
/// If there are multiple recipients this is just a random one, so is not very useful.
|
||||
pub fn recipient(&self) -> EmailAddress {
|
||||
let raw = self
|
||||
.params
|
||||
.get(Param::Recipients)
|
||||
.expect("no recipients in params");
|
||||
let rcpt = raw.split(' ').next().expect("no recipient found");
|
||||
rcpt.parse().expect("failed to parse email address")
|
||||
}
|
||||
|
||||
/// The raw message payload.
|
||||
pub fn payload(&self) -> String {
|
||||
std::fs::read_to_string(&self.blob_path).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a pre-generated keypair for alice@example.com from disk.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use deltachat::config;
|
||||
use deltachat::context::*;
|
||||
use tempfile::tempdir;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
/* some data used for testing
|
||||
******************************************************************************/
|
||||
@@ -92,19 +92,26 @@ async fn stress_functions(context: &Context) {
|
||||
// free(qr.cast());
|
||||
}
|
||||
|
||||
async fn create_test_context() -> Context {
|
||||
#[allow(dead_code)]
|
||||
struct TestContext {
|
||||
ctx: Context,
|
||||
dir: TempDir,
|
||||
}
|
||||
|
||||
async fn create_test_context() -> TestContext {
|
||||
use rand::Rng;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = rand::thread_rng().gen();
|
||||
Context::new("FakeOs".into(), dbfile.into(), id)
|
||||
let ctx = Context::new("FakeOs".into(), dbfile.into(), id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
TestContext { ctx, dir }
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_stress_tests() {
|
||||
let context = create_test_context().await;
|
||||
stress_functions(&context).await;
|
||||
stress_functions(&context.ctx).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user