Compare commits

..

4 Commits

Author SHA1 Message Date
Hocuri
db5cb45b9c Rustfmt, Comment, move a little bit slower 2020-10-12 16:05:18 +02:00
Hocuri
36046f5f2c Repair some tests 2020-10-12 15:33:54 +02:00
Hocuri
1f9c0ef7d9 Remove debug logs 2020-10-12 15:33:53 +02:00
Hocuri
efd62a7c04 Completely smooth progress bar 2020-10-12 15:33:49 +02:00
54 changed files with 1695 additions and 3334 deletions

View File

@@ -1,135 +1,5 @@
# Changelog
## 1.50.0
- do not fetch emails in between inbox_watch disabled and enabled again #2087
- fix: do not fetch from INBOX if inbox_watch is disabled #2085
- fix: do not use STARTTLS when PLAIN connection is requested
and do not allow downgrade if STARTTLS is not available #2071
## 1.49.0
- add timestamps to image and video filenames #2068
- forbid quoting messages from another context #2069
- fix: preserve quotes in messages with attachments #2070
## 1.48.0
- `fetch_existing` renamed to `fetch_existing_msgs` and disabled by default
#2035 #2042
- skip fetch existing messages/contacts if config-option `bot` set #2017
- always log why a message is sorted to trash #2045
- display a quote if top posting is detected #2047
- add ephemeral task cancellation to `dc_stop_io()`;
before, there was no way to quickly terminate pending ephemeral tasks #2051
- when saved-messages chat is deleted,
a device-message about recreation is added #2050
- use `max_smtp_rcpt_to` from provider-db,
sending messages to many recipients in configurable chunks #2056
- fix handling of empty autoconfigure files #2027
- fix adding saved messages to wrong chats on multi-device #2034 #2039
- fix hang on android4.4 and other systems
by adding a workaround to executer-blocking-handling bug #2040
- fix secret key export/import roundtrip #2048
- fix mistakenly unarchived chats #2057
- fix outdated-reminder test that fails only 7 days a year,
including halloween :) #2059
- improve python bindings #2021 #2036 #2038
- update provider-database #2037
## 1.47.0
- breaking change: `dc_update_device_chats()` removed;
this is now done automatically during configure
unless the new config-option `bot` is set #1957
- breaking change: split `DC_EVENT_MSGS_NOTICED` off `DC_EVENT_MSGS_CHANGED`
and remove `dc_marknoticed_all_chats()` #1942 #1981
- breaking change: remove unused starring options #1965
- breaking change: `DC_CHAT_TYPE_VERIFIED_GROUP` replaced by
`dc_chat_is_protected()`; also single-chats may be protected now, this may
happen over the wire even if the UI do not offer an option for that #1968
- breaking change: split quotes off message text,
UIs should use at least `dc_msg_get_quoted_text()` to show quotes now #1975
- new api for quote handling: `dc_msg_set_quote()`, `dc_msg_get_quoted_text()`,
`dc_msg_get_quoted_msg()` #1975 #1984 #1985 #1987 #1989 #2004
- require quorum to enable encryption #1946
- speed up and clean up account creation #1912 #1927 #1960 #1961
- configure now collects recent contacts and fetches last messages
unless disabled by `fetch_existing` config-option #1913 #2003
EDIT: `fetch_existing` renamed to `fetch_existing_msgs` in 1.48.0 #2042
- emit `DC_EVENT_CHAT_MODIFIED` on contact rename
and set contact-id on `DC_EVENT_CONTACTS_CHANGED` #1935 #1936 #1937
- add `dc_set_chat_protection()`; the `protect` parameter in
`dc_create_group_chat()` will be removed in an upcoming release;
up to then, UIs using the "verified group" paradigm
should not use `dc_set_chat_protection()` #1968 #2014 #2001 #2012 #2007
- remove unneeded `DC_STR_COUNT` #1991
- mark all failed messages as failed when receiving an NDN #1993
- check some easy cases for bad system clock and outdated app #1901
- fix import temporary directory usage #1929
- fix forcing encryption for reset peers #1998
- fix: do not allow to save drafts in non-writeable chats #1997
- fix: do not show HTML if there is no content and there is an attachment #1988
- fix recovering offline/lost connections, fixes background receive bug #1983
- fix ordering of accounts returned by `dc_accounts_get_all()` #1909
- fix whitespace for summaries #1938
- fix: improve sentbox name guessing #1941
- fix: avoid manual poll impl for accounts events #1944
- fix encoding newlines in param as a preparation for storing quotes #1945
- fix: internal and ffi error handling #1967 #1966 #1959 #1911 #1916 #1917 #1915
- fix ci #1928 #1931 #1932 #1933 #1934 #1943
- update provider-database #1940 #2005 #2006
- update dependencies #1919 #1908 #1950 #1963 #1996 #2010 #2013
## 1.46.0
- breaking change: `dc_configure()` report errors in
@@ -875,3 +745,4 @@
For a full list of changes, please see our closed Pull Requests:
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed

238
Cargo.lock generated
View File

@@ -224,13 +224,13 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.4.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bdc2bb91844456110d27da8f712f4cbc693f3538521a43cb79c6b2e6f717f3e"
checksum = "caadebe62f5ad78d6418a47a86b5b84439a52bdd5c893e55a5c81974da111455"
dependencies = [
"async-native-tls",
"async-std",
"base64 0.13.0",
"base64 0.12.3",
"byte-pool",
"chrono",
"futures",
@@ -446,33 +446,6 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64-stream"
version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6596cd4b981cb9e85a9bddf2d1c3a76ebffe64f9e6b0ea6f053d810aea7807c"
dependencies = [
"base64 0.13.0",
"educe",
"generic-array",
]
[[package]]
name = "bincode"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d"
dependencies = [
"byteorder",
"serde",
]
[[package]]
name = "bit-set"
version = "0.5.2"
@@ -706,16 +679,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "chrono-tz"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120"
dependencies = [
"chrono",
"parse-zoneinfo",
]
[[package]]
name = "circular"
version = "0.3.0"
@@ -843,7 +806,7 @@ dependencies = [
"clap",
"criterion-plot",
"csv",
"itertools 0.9.0",
"itertools",
"lazy_static",
"num-traits",
"oorandom",
@@ -865,7 +828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d"
dependencies = [
"cast",
"itertools 0.9.0",
"itertools",
]
[[package]]
@@ -1022,12 +985,6 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d0e2d24e5ee3b23a01de38eefdcd978907890701f08ffffd4cb457ca4ee8d6"
[[package]]
name = "debug-helper"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8a5bb894f24f42c247f19b25928a88e31867c0f84552c05df41a9dd527435e"
[[package]]
name = "deflate"
version = "0.8.6"
@@ -1040,7 +997,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.50.0"
version = "1.46.0"
dependencies = [
"ansi_term 0.12.1",
"anyhow",
@@ -1066,11 +1023,11 @@ dependencies = [
"futures",
"futures-lite",
"hex",
"ical",
"image",
"indexmap",
"itertools 0.9.0",
"itertools",
"kamadak-exif",
"lazy_static",
"lettre_email",
"libc",
"log",
@@ -1078,13 +1035,12 @@ dependencies = [
"native-tls",
"num-derive",
"num-traits",
"once_cell",
"percent-encoding",
"pgp",
"pretty_assertions",
"pretty_env_logger",
"proptest",
"quick-xml 0.18.1",
"quick-xml",
"r2d2",
"r2d2_sqlite",
"rand",
@@ -1105,7 +1061,6 @@ dependencies = [
"toml",
"url",
"uuid",
"vcard",
]
[[package]]
@@ -1118,7 +1073,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.50.0"
version = "1.46.0"
dependencies = [
"anyhow",
"async-std",
@@ -1248,18 +1203,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "educe"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7260c7e6e656fc7702a1aa8d5b498a1a69aa84ac4ffcd5501b7d26939f368a93"
dependencies = [
"enum-ordinalize",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "either"
version = "1.6.1"
@@ -1386,19 +1329,6 @@ dependencies = [
"syn",
]
[[package]]
name = "enum-ordinalize"
version = "3.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1676e1daadfd216bda88d3a6fedd1bf53b829a085f5cc4d81c6f3054f50ef983"
dependencies = [
"num-bigint 0.3.1",
"num-traits",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "env_logger"
version = "0.7.1"
@@ -1427,28 +1357,6 @@ version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59"
[[package]]
name = "failure"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
dependencies = [
"backtrace",
"failure_derive",
]
[[package]]
name = "failure_derive"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
@@ -1840,15 +1748,6 @@ dependencies = [
"quick-error",
]
[[package]]
name = "ical"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a9f7215ad0d77e69644570dee000c7678a47ba7441062c1b5f918adde0d73cf"
dependencies = [
"thiserror",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -1884,9 +1783,9 @@ dependencies = [
[[package]]
name = "imap-proto"
version = "0.11.0"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3091b99ee5b80f9b010eb6f962af9495ad06561bf662126b077e8ca30e463182"
checksum = "16a6def1d5ac8975d70b3fd101d57953fe3278ef2ee5d7816cba54b1d1dfc22f"
dependencies = [
"nom 5.1.2",
]
@@ -1928,15 +1827,6 @@ dependencies = [
"winreg",
]
[[package]]
name = "itertools"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.9.0"
@@ -2265,17 +2155,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf"
dependencies = [
"autocfg 1.0.1",
"num-integer",
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.6.0"
@@ -2369,12 +2248,6 @@ version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad"
[[package]]
name = "oncemutex"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2"
[[package]]
name = "oorandom"
version = "11.1.2"
@@ -2495,15 +2368,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "parse-zoneinfo"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
dependencies = [
"regex",
]
[[package]]
name = "pem"
version = "0.8.1"
@@ -2570,26 +2434,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "phonenumber"
version = "0.2.4+8.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "207b3a8b4b30da166f6d8175ad39b86aa84d190965be4533af3d5541754de358"
dependencies = [
"bincode",
"either",
"failure",
"fnv",
"itertools 0.8.2",
"lazy_static",
"nom 5.1.2",
"quick-xml 0.17.2",
"regex",
"regex-cache",
"serde",
"serde_derive",
]
[[package]]
name = "pin-project"
version = "0.4.25"
@@ -2750,15 +2594,6 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0"
dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.18.1"
@@ -2918,18 +2753,6 @@ dependencies = [
"byteorder",
]
[[package]]
name = "regex-cache"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c86351f6af6bbf23b4c5f73ee4fdfe92d298fdf28572ea4f69494cabe38699"
dependencies = [
"lru-cache",
"oncemutex",
"regex",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.18"
@@ -3103,9 +2926,9 @@ dependencies = [
[[package]]
name = "sanitize-filename"
version = "0.3.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf18934a12018228c5b55a6dae9df5d0641e3566b3630cb46cc55564068e7c2f"
checksum = "23fd0fec94ec480abfd86bb8f4f6c57e0efb36dac5c852add176ea7b04c74801"
dependencies = [
"lazy_static",
"regex",
@@ -3308,7 +3131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b"
dependencies = [
"chrono",
"num-bigint 0.2.6",
"num-bigint",
"num-traits",
]
@@ -3788,37 +3611,6 @@ dependencies = [
"serde",
]
[[package]]
name = "validators"
version = "0.20.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9af4acf80e223db35334bfb3b09421c54ed259ca203b5189177e61e3b9e1c2a3"
dependencies = [
"debug-helper",
"lazy_static",
"num-traits",
"phonenumber",
"regex",
]
[[package]]
name = "vcard"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c988b5461430fd8d078237c8a9436c8e2f810f944cba3086e3ce99cd54652caf"
dependencies = [
"base64-stream",
"chrono",
"chrono-tz",
"idna",
"lazy_static",
"mime",
"mime_guess",
"percent-encoding",
"regex",
"validators",
]
[[package]]
name = "vcpkg"
version = "0.2.10"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.50.0"
version = "1.46.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -34,7 +34,7 @@ serde_json = "1.0"
chrono = "0.4.6"
indexmap = "1.3.0"
kamadak-exif = "0.5"
once_cell = "1.4.1"
lazy_static = "1.4.0"
regex = "1.1.6"
rusqlite = { version = "0.24", features = ["bundled"] }
r2d2_sqlite = "0.17.0"
@@ -47,7 +47,7 @@ itertools = "0.9.0"
quick-xml = "0.18.1"
escaper = "0.1.0"
bitflags = "1.1.0"
sanitize-filename = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.13.0"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
@@ -61,8 +61,6 @@ url = "2.1.1"
async-std-resolver = "0.19.5"
async-tar = "0.3.0"
uuid = { version = "0.8", features = ["serde", "v4"] }
vcard = "0.4.6"
ical = { version = "0.7.0", default-features = false, features = ["vcard"] }
pretty_env_logger = { version = "0.4.0", optional = true }
log = {version = "0.4.8", optional = true }

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@ use async_std::task::{block_on, spawn};
use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::accounts::Accounts;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, Origin};
use deltachat::context::Context;
@@ -807,6 +807,21 @@ pub unsafe extern "C" fn dc_add_device_msg(
.to_u32()
}
#[no_mangle]
pub unsafe extern "C" fn dc_update_device_chats(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_update_device_chats()");
return;
}
let ctx = &mut *context;
block_on(async move {
ctx.update_device_chats()
.await
.unwrap_or_log_default(&ctx, "Failed to add device message")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_was_device_msg_ever_added(
context: *mut dc_context_t,
@@ -1042,32 +1057,6 @@ pub unsafe extern "C" fn dc_get_next_media(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_protection(
context: *mut dc_context_t,
chat_id: u32,
protect: libc::c_int,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_protection()");
return 0;
}
let ctx = &*context;
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
s
} else {
warn!(ctx, "bad protect-value for dc_set_chat_protection()");
return 0;
};
block_on(async move {
match ChatId::new(chat_id).set_protection(&ctx, protect).await {
Ok(()) => 1,
Err(_) => 0,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_visibility(
context: *mut dc_context_t,
@@ -1181,7 +1170,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
#[no_mangle]
pub unsafe extern "C" fn dc_create_group_chat(
context: *mut dc_context_t,
protect: libc::c_int,
verified: libc::c_int,
name: *const libc::c_char,
) -> u32 {
if context.is_null() || name.is_null() {
@@ -1189,15 +1178,14 @@ pub unsafe extern "C" fn dc_create_group_chat(
return 0;
}
let ctx = &*context;
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
let verified = if let Some(s) = contact::VerifiedStatus::from_i32(verified) {
s
} else {
warn!(ctx, "bad protect-value for dc_create_group_chat()");
return 0;
};
block_on(async move {
chat::create_group_chat(&ctx, protect, to_string_lossy(name))
chat::create_group_chat(&ctx, verified, to_string_lossy(name))
.await
.log_err(ctx, "Failed to create group chat")
.map(|id| id.to_u32())
@@ -1727,15 +1715,13 @@ pub unsafe extern "C" fn dc_imex(
let ctx = &*context;
if let Some(param1) = to_opt_string_lossy(param1) {
spawn(async move {
imex::imex(&ctx, what, &param1)
.await
.log_err(ctx, "IMEX failed")
});
} else {
eprintln!("dc_imex called without a valid directory");
}
let param1 = to_opt_string_lossy(param1);
spawn(async move {
imex::imex(&ctx, what, param1)
.await
.log_err(ctx, "IMEX failed")
});
}
#[no_mangle]
@@ -2403,13 +2389,13 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_int {
pub unsafe extern "C" fn dc_chat_is_verified(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_protected()");
eprintln!("ignoring careless call to dc_chat_is_verified()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_protected() as libc::c_int
ffi_chat.chat.is_verified() as libc::c_int
}
#[no_mangle]
@@ -2831,16 +2817,6 @@ pub unsafe extern "C" fn dc_msg_is_info(msg: *mut dc_msg_t) -> libc::c_int {
ffi_msg.message.is_info().into()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_info_type()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_info_type() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
@@ -3008,11 +2984,6 @@ pub unsafe extern "C" fn dc_msg_set_quote(msg: *mut dc_msg_t, quote: *const dc_m
let ffi_msg = &mut *msg;
let ffi_quote = &*quote;
if ffi_msg.context != ffi_quote.context {
eprintln!("ignoring attempt to quote message from a different context");
return;
}
block_on(async move {
ffi_msg
.message

View File

@@ -5,95 +5,69 @@ Problem: missing eventual group consistency
If group members are concurrently adding new members,
the new members will miss each other's additions, example:
1. Alice and Bob are in a two-member group
- Alice and Bob are in a two-member group
2. Then Alice adds Carol, while concurrently Bob adds Doris
- Alice adds Carol, concurrently Bob adds Doris
Right now, the group has inconsistent memberships:
- Carol will see a three-member group (Alice, Bob, Carol),
Doris will see a different three-member group (Alice, Bob, Doris),
and only Alice and Bob will have all four members.
- Alice and Carol see a (Alice, Carol, Bob) group
- Bob and Doris see a (Bob, Doris, Alice)
This then leads to "sender is unknown" messages in the chat,
for example when Alice receives a message from Doris,
or when Bob receives a message from Carol.
There are also other sources for group membership inconsistency:
- leaving/deleting/adding in larger groups, while being offline,
increases chances for inconsistent group membership
- dropped group-membership messages
- group-membership messages landing in "Spam"
Note that for verified groups any mitigation mechanism likely
needs to make all clients to know who originally added a member.
Note that all these problems (can) also happen with verified groups,
then raising "false alarms" which could lure people to ignore such issues.
solution: memorize+attach (possible encrypted) chat-meta mime messages
----------------------------------------------------------------------
IOW, it's clear we need to do something about it to improve overall
reliability in group-settings.
For reference, please see https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members how MemberAdded/Removed messages are shaped.
- All Chat-Group-Member-Added/Removed messages are recorded in their
full raw (signed and encrypted) mime-format in the DB
Solution: replay group modification messages on inconsistencies
------------------------------------------------------------------
- If an incoming member-add/member-delete messages has a member list
which is, apart from the added/removed member, not consistent
with our own view, broadcast a "Chat-Group-Member-Correction" message to
all members, attaching the original added/removed mime-message for all mismatching
contacts. If we have no relevant add/del information, don't send a
correction message out.
For brevity let's abbreviate "group membership modification" as **GMM**.
- Upong receiving added/removed attachments we don't do the
check_consistency+correction message dance.
This avoids recursion problems and hard-to-reason-about chatter.
Delta chat has explicit GMM messages, typically encrypted to the group members
as seen by the device that sends the GMM. The `Spec <https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members>`_ details the Mime headers and format.
Notes:
If we detect membership inconsistencies we can resend relevant GMM messages
to the respective chat. The receiving devices can process those GMM messages
as if it would be an incoming message. If for example they have already seen
the Message-ID of the GMM message, they will ignore the message. It's
probably useful to record GMM message in their original MIME-format and
not invent a new recording format. Few notes on three aspects:
- mechanism works for both encrypted and unencrypted add/del messages
- **group-membership-tracking**: All valid GMM messages are persisted in
their full raw (signed/encrypted?) MIME-format in the DB. Note that GMM messages
already are in the msgs table, and there is a mime_header column which we could
extend to contain the raw Mime GMM message.
- we already have a "mime_headers" column in the DB for each incoming message.
We could extend it to also include the payload and store mime unconditionally
for member-added/removed messages.
- **consistency_checking**: If an incoming GMM has a member list which is
not consistent with our own view, broadcast a "Group-Member-Correction"
message to all members containing a multipart list of GMMs.
- multiple member-added/removed messages can be attached in a single
correction message
- **correcting_memberships**: Upon receiving a Group-Member-Correction
message we pass the contained GMMs to the "incoming mail pipeline"
(without **consistency_checking** them, to avoid recursion issues)
- it is minimal on the number of overall messages to reach group consistency
(best-case: no extra messages, the ABCD case above: max two extra messages)
- somewhat backward compatible: older clients will probably ignore
messages which are signed by someone who is not the outer From-address.
Alice/Carol and Bob/Doris getting on the same page
++++++++++++++++++++++++++++++++++++++++++++++++++
Recall that Alice/Carol and Bob/Doris had a differening view of
group membership. With the proposed solution, when Bob receives
Alice's "Carol added" message, he will notice that Alice (and thus
also carol) did not know about Doris. Bob's device sends a
"Chat-Group-Member-Correction" message containing his own GMM
when adding Doris. Therefore, the group's membership is healed
for everyone in a single broadcast message.
Alice might also send a Group-member-Correction message,
so there is a second chance that the group gets to know all GMMs.
Note, for example, that if for some reason Bobs and Carols provider
drop GMM messages between them (spam) that Alice and Doris can heal
it by resending GMM messages whenever they detect them to be out of sync.
- the correction-protocol also helps with dropped messages. If a member
did not see a member-added/removed message, the next member add/removed
message in the group will likely heal group consistency for this member.
- we can quite easily extend the mechanism to also provide the group-avatar or
other meta-information.
Discussions of variants
++++++++++++++++++++++++
- instead of acting on GMM messages we could send corrections
for any received message that addresses inconsistent group members but
a) this could delay group-membership healing
- instead of acting on MemberAdded/Removed message we could send
corrections for any received message that addresses inconsistent group members but
a) this would delay group-membership healing
b) could lead to a lot of members sending corrections
c) means we might rely on "To-Addresses" which we also like to strike
at least for protected chats.
- instead of broadcasting correction messages we could only send it to
the sender of the inconsistent member-added/removed message.
@@ -109,3 +83,44 @@ Discussions of variants
while both being in an offline or bad-connection situation).
solution2: repeat member-added/removed messages
---------------------------------------------------
Introduce a new Chat-Group-Member-Changed header and deprecate Chat-Group-Member-Added/Removed
but keep sending out the old headers until the new protocol is sufficiently deployed.
The new Chat-Group-Member-Changed header contains a Time-to-Live number (TTL)
which controls repetition of the signed "add/del e-mail address" payload.
Example::
Chat-Group-Member-Changed: TTL add "somedisplayname" someone@example.org
owEBYQGe/pANAwACAY47A6J5t3LWAcsxYgBeTQypYWRkICJzb21lZGlzcGxheW5h
bWUiIHNvbWVvbmVAZXhhbXBsZS5vcmcgCokBHAQAAQIABgUCXk0MqQAKCRCOOwOi
ebdy1hfRB/wJ74tgFQulicthcv9n+ZsqzwOtBKMEVIHqJCzzDB/Hg/2z8ogYoZNR
iUKKrv3Y1XuFvdKyOC+wC/unXAWKFHYzY6Tv6qDp6r+amt+ad+8Z02q53h9E55IP
FUBdq2rbS8hLGjQB+mVRowYrUACrOqGgNbXMZjQfuV7fSc7y813OsCQgi3tjstup
b+uduVzxCp3PChGhcZPs3iOGCnQvSB8VAaLGMWE2d7nTo/yMQ0Jx69x5qwfXogTk
mTt5rOJyrosbtf09TMKFzGgtqBcEqHLp3+mQpZQ+WHUKAbsRa8Jc9DOUOSKJ8SNM
clKdskprY+4LY0EBwLD3SQ7dPkTITCRD
=P6GG
TTL is set to "2" on an initial Chat-Group-Member-Changed add/del message.
Receivers will apply the add/del change to the group-membership,
decrease the TTL by 1, and if TTL>0 re-sent the header.
The "add|del e-mail address" payload is pgp-signed and repeated verbatim.
This allows to propagate, in a cryptographically secured way,
who added a member. This is particularly important for allowing
to show in verified groups who added a member (planned).
Disadvantage to solution 1:
- requires to specify encoding and precise rules for what/how is signed.
- causes O(N^2) extra messages
- Not easily extendable for other things (without introducing a new
header / encoding)

View File

@@ -4,7 +4,7 @@ use std::str::FromStr;
use anyhow::{bail, ensure};
use async_std::path::Path;
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, ProtectionStatus};
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility};
use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
@@ -357,7 +357,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
createchat <contact-id>\n\
createchatbymsg <msg-id>\n\
creategroup <name>\n\
createprotected <name>\n\
createverified <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
groupname <name>\n\
@@ -379,8 +379,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unarchive <chat-id>\n\
pin <chat-id>\n\
unpin <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
@@ -443,20 +441,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"export-backup" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportBackup, &dir).await?;
imex(&context, ImexMode::ExportBackup, Some(&dir)).await?;
println!("Exported to {}.", dir.to_string_lossy());
}
"import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
imex(&context, ImexMode::ImportBackup, arg1).await?;
imex(&context, ImexMode::ImportBackup, Some(arg1)).await?;
}
"export-keys" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportSelfKeys, &dir).await?;
imex(&context, ImexMode::ExportSelfKeys, Some(&dir)).await?;
println!("Exported to {}.", dir.to_string_lossy());
}
"import-keys" => {
imex(&context, ImexMode::ImportSelfKeys, arg1).await?;
imex(&context, ImexMode::ImportSelfKeys, Some(arg1)).await?;
}
"export-setup" => {
let setup_code = create_setup_code(&context);
@@ -525,7 +523,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
println!(
"{}#{}: {} [{} fresh] {}{}",
"{}#{}: {} [{} fresh] {}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
@@ -535,7 +533,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
if chat.is_protected() { "🛡️" } else { "" },
);
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
let statestr = if chat.visibility == ChatVisibility::Archived {
@@ -610,7 +607,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
format!("{} member(s)", members.len())
};
println!(
"{}#{}: {} [{}]{}{} {}",
"{}#{}: {} [{}]{}{}",
chat_prefix(sel_chat),
sel_chat.get_id(),
sel_chat.get_name(),
@@ -627,11 +624,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
_ => "".to_string(),
},
if sel_chat.is_protected() {
"🛡️"
} else {
""
},
);
log_msglist(&context, &msglist).await?;
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
@@ -662,16 +654,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
chat::create_group_chat(&context, VerifiedStatus::Unverified, arg1).await?;
println!("Group#{} created successfully.", chat_id);
}
"createprotected" => {
"createverified" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?;
println!("Group#{} created and protected successfully.", chat_id);
println!("VerifiedGroup#{} created successfully.", chat_id);
}
"addmember" => {
ensure!(sel_chat.is_some(), "No chat selected");
@@ -881,6 +872,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"updatedevicechats" => {
context.update_device_chats().await?;
}
"listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -912,21 +906,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned,
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
}
"protect" | "unprotect" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id
.set_protection(
&context,
match arg0 {
"protect" => ProtectionStatus::Protected,
"unprotect" => ProtectionStatus::Unprotected,
_ => unreachable!("arg0={:?}", arg0),
_ => panic!("Unexpected command (This should never happen)"),
},
)
.await?;

View File

@@ -197,12 +197,14 @@ const CHAT_COMMANDS: [&str; 27] = [
"unpin",
"delchat",
];
const MESSAGE_COMMANDS: [&str; 6] = [
const MESSAGE_COMMANDS: [&str; 8] = [
"listmsgs",
"msginfo",
"listfresh",
"forward",
"markseen",
"star",
"unstar",
"delmsg",
];
const CONTACT_COMMANDS: [&str; 6] = [

View File

@@ -77,7 +77,6 @@ def run_cmdline(argv=None, account_plugins=None):
ac.set_config("mvbox_move", "0")
ac.set_config("mvbox_watch", "0")
ac.set_config("sentbox_watch", "0")
ac.set_config("bot", "1")
configtracker = ac.configure()
configtracker.wait_finish()

View File

@@ -214,19 +214,6 @@ class Account(object):
:param name: (optional) display name for this contact
:returns: :class:`deltachat.contact.Contact` instance.
"""
(name, addr) = self.get_contact_addr_and_name(obj, name)
name = as_dc_charpointer(name)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
return Contact(self, contact_id)
def get_contact(self, obj):
if isinstance(obj, Contact):
return obj
(_, addr) = self.get_contact_addr_and_name(obj)
return self.get_contact_by_addr(addr)
def get_contact_addr_and_name(self, obj, name=None):
if isinstance(obj, Account):
if not obj.is_configured():
raise ValueError("can only add addresses from configured accounts")
@@ -242,7 +229,13 @@ class Account(object):
if name is None and displayname:
name = displayname
return (name, addr)
return self._create_contact(addr, name)
def _create_contact(self, addr, name):
addr = as_dc_charpointer(addr)
name = as_dc_charpointer(name)
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
return Contact(self, contact_id)
def delete_contact(self, contact):
""" delete a Contact.
@@ -270,17 +263,6 @@ class Account(object):
"""
return Contact(self, contact_id)
def get_blocked_contacts(self):
""" return a list of all blocked contacts.
:returns: list of :class:`deltachat.contact.Contact` objects.
"""
dc_array = ffi.gc(
lib.dc_get_blocked_contacts(self._dc_context),
lib.dc_array_unref
)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_contacts(self, query=None, with_self=False, only_verified=False):
""" get a (filtered) list of contacts.
@@ -354,9 +336,6 @@ class Account(object):
def get_deaddrop_chat(self):
return Chat(self, const.DC_CHAT_ID_DEADDROP)
def get_device_chat(self):
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
def get_message_by_id(self, msg_id):
""" return Message instance.
:param msg_id: integer id of this message.

View File

@@ -57,7 +57,10 @@ class Chat(object):
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
"""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
return lib.dc_chat_get_type(self._dc_chat) in (
const.DC_CHAT_TYPE_GROUP,
const.DC_CHAT_TYPE_VERIFIED_GROUP
)
def is_deaddrop(self):
""" return true if this chat is a deaddrop chat.
@@ -82,20 +85,12 @@ class Chat(object):
"""
return not lib.dc_chat_is_unpromoted(self._dc_chat)
def can_send(self):
"""Check if messages can be sent to a give chat.
This is not true eg. for the deaddrop or for the device-talk
def is_verified(self):
""" return True if this chat is a verified group.
:returns: True if the chat is writable, False otherwise
:returns: True if chat is verified, False otherwise.
"""
return lib.dc_chat_can_send(self._dc_chat)
def is_protected(self):
""" return True if this chat is a protected chat.
:returns: True if chat is protected, False otherwise.
"""
return lib.dc_chat_is_protected(self._dc_chat)
return lib.dc_chat_is_verified(self._dc_chat)
def get_name(self):
""" return name of this chat.
@@ -371,7 +366,7 @@ class Chat(object):
:raises ValueError: if contact could not be removed
:returns: None
"""
contact = self.account.get_contact(obj)
contact = self.account.create_contact(obj)
ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError("could not remove contact {!r} from chat".format(contact))

View File

@@ -52,17 +52,9 @@ class Contact(object):
return lib.dc_contact_is_blocked(self._dc_contact)
def set_blocked(self, block=True):
""" [Deprecated, use block/unblock methods] Block or unblock a contact. """
""" Block or unblock a contact. """
return lib.dc_block_contact(self.account._dc_context, self.id, block)
def block(self):
""" Block this contact. Message will not be seen/retrieved from this contact. """
return lib.dc_block_contact(self.account._dc_context, self.id, True)
def unblock(self):
""" Unblock this contact. Messages from this contact will be retrieved (again)."""
return lib.dc_block_contact(self.account._dc_context, self.id, False)
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)

View File

@@ -103,14 +103,6 @@ class FFIEventTracker:
if rex.search(ev.data2):
return ev
def get_info_regex_groups(self, regex, check_error=True):
rex = re.compile(regex)
while 1:
ev = self.get_matching("DC_EVENT_INFO", check_error=check_error)
m = rex.match(ev.data2)
if m is not None:
return m.groups()
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile("(?:{}).*".format(event_name_regex))

View File

@@ -330,13 +330,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
return accounts
def clone_online_account(self, account, pre_generated_key=True):
""" Clones addr, mail_pw, mvbox_watch, mvbox_move, sentbox_watch and the
direct_imap object of an online account. This simulates the user setting
up a new device without importing a backup.
`pre_generated_key` only means that a key from python/tests/data/key is
used in order to speed things up.
"""
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
@@ -361,8 +354,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
for acc in self._accounts:
if hasattr(acc, "_configtracker"):
acc._configtracker.wait_finish()
acc._evtracker.consume_events()
acc.get_device_chat().mark_noticed()
del acc._configtracker
acc.set_config("bcc_self", "0")
if acc.is_configured() and not acc.is_started():
@@ -510,9 +501,6 @@ def lp():
def step(self, msg):
print("-" * 5, "step " + msg, "-" * 5)
def indent(self, msg):
print(" " + msg)
return Printer()

View File

@@ -129,20 +129,6 @@ class TestOfflineContact:
assert not contact1.is_blocked()
assert not contact1.is_verified()
def test_get_blocked(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
contact2 = ac1.create_contact("some2@example.org", name="some2")
ac1.create_contact("some3@example.org", name="some3")
assert ac1.get_blocked_contacts() == []
contact1.block()
assert ac1.get_blocked_contacts() == [contact1]
contact2.block()
blocked = ac1.get_blocked_contacts()
assert len(blocked) == 2 and contact1 in blocked and contact2 in blocked
contact2.unblock()
assert ac1.get_blocked_contacts() == [contact1]
def test_create_self_contact(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
contact1 = ac1.create_contact(ac1.get_config("addr"))
@@ -288,28 +274,6 @@ class TestOfflineChat:
qr = chat.get_join_qr()
assert ac2.check_qr(qr).is_ask_verifygroup
def test_removing_blocked_user_from_group(self, ac1, lp):
"""
Test that blocked contact is not unblocked when removed from a group.
See https://github.com/deltachat/deltachat-core-rust/issues/2030
"""
lp.sec("Create a group chat with a contact")
contact = ac1.create_contact("some1@example.org")
group = ac1.create_group_chat("title", contacts=[contact])
group.send_text("First group message")
lp.sec("ac1 blocks contact")
contact.block()
assert contact.is_blocked()
lp.sec("ac1 removes contact from their group")
group.remove_contact(contact)
assert contact.is_blocked()
lp.sec("ac1 adding blocked contact unblocks it")
group.add_contact(contact)
assert not contact.is_blocked()
def test_get_set_profile_image_simple(self, ac1, data):
chat = ac1.create_group_chat(name="title1")
p = data.get_path("d.png")
@@ -668,7 +632,7 @@ class TestOnlineAccount:
except Exception:
pass
def test_export_import_self_keys(self, acfactory, tmpdir, lp):
def test_export_import_self_keys(self, acfactory, tmpdir):
ac1, ac2 = acfactory.get_two_online_accounts()
dir = tmpdir.mkdir("exportdir")
@@ -676,17 +640,8 @@ class TestOnlineAccount:
assert len(export_files) == 2
for x in export_files:
assert x.startswith(dir.strpath)
key_id, = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
ac1._evtracker.consume_events()
lp.sec("exported keys (private and public)")
for name in os.listdir(dir.strpath):
lp.indent(dir.strpath + os.sep + name)
lp.sec("importing into existing account")
ac2.import_self_keys(dir.strpath)
key_id2, = ac2._evtracker.get_info_regex_groups(
r".*stored.*KeyId\((.*)\).*", check_error=False)
assert key_id2 == key_id
def test_one_account_send_bcc_setting(self, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
@@ -969,30 +924,6 @@ class TestOnlineAccount:
except queue.Empty:
pass # mark_seen_messages() has generated events before it returns
def test_reply_privately(self, acfactory):
ac1, ac2 = acfactory.get_two_online_accounts()
group1 = ac1.create_group_chat("group")
group1.add_contact(ac2)
group1.send_text("hello")
msg2 = ac2._evtracker.wait_next_messages_changed()
group2 = msg2.create_chat()
assert group2.get_name() == group1.get_name()
msg_reply = Message.new_empty(ac2, "text")
msg_reply.set_text("message reply")
msg_reply.quote = msg2
private_chat1 = ac1.create_chat(ac2)
private_chat2 = ac2.create_chat(ac1)
private_chat2.send_msg(msg_reply)
msg_reply1 = ac1._evtracker.wait_next_incoming_message()
assert msg_reply1.quoted_text == "hello"
assert not msg_reply1.chat.is_group()
assert msg_reply1.chat.id == private_chat1.id
def test_mdn_asymetric(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts(move=True)
@@ -1101,16 +1032,6 @@ class TestOnlineAccount:
assert msg_in.text == text2
assert ac1.get_config("addr") in [x.addr for x in msg_in.chat.get_contacts()]
def test_no_draft_if_cant_send(self, acfactory):
"""Tests that no quote can be set if the user can't send to this chat"""
ac1 = acfactory.get_one_online_account()
device_chat = ac1.get_device_chat()
msg = Message.new_empty(ac1, "text")
device_chat.set_draft(msg)
assert not device_chat.can_send()
assert device_chat.get_draft() is None
def test_prefer_encrypt(self, acfactory, lp):
"""Test quorum rule for encryption preference in 1:1 and group chat."""
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
@@ -1161,8 +1082,8 @@ class TestOnlineAccount:
# Majority prefers encryption now
assert msg5.is_encrypted()
def test_quote_encrypted(self, acfactory, lp):
"""Test that replies to encrypted messages with quotes are encrypted."""
def test_reply_encrypted(self, acfactory, lp):
"""Test that replies to encrypted messages are encrypted."""
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")
@@ -1211,39 +1132,6 @@ class TestOnlineAccount:
assert msg_in.quoted_text == quoted_msg.text
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
def test_quote_attachment(self, tmpdir, acfactory, lp):
"""Test that replies with an attachment and a quote are received correctly."""
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1 creates chat with ac2")
chat1 = ac1.create_chat(ac2)
lp.sec("ac1 sends text message to ac2")
chat1.send_text("hi")
lp.sec("ac2 receives contact request from ac1")
received_message = ac2._evtracker.wait_next_messages_changed()
assert received_message.text == "hi"
basename = "attachment.txt"
p = os.path.join(tmpdir.strpath, basename)
with open(p, "w") as f:
f.write("data to send")
lp.sec("ac2 sends a reply to ac1")
chat2 = received_message.create_chat()
reply = Message.new_empty(ac2, "file")
reply.set_text("message reply")
reply.set_file(p)
reply.quote = received_message
chat2.send_msg(reply)
lp.sec("ac1 receives a reply from ac2")
received_reply = ac1._evtracker.wait_next_incoming_message()
assert received_reply.text == "message reply"
assert received_reply.quoted_text == received_message.text
assert open(received_reply.filename).read() == "data to send"
def test_saved_mime_on_received_message(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1455,7 +1343,7 @@ class TestOnlineAccount:
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_protected()
assert chat1.is_verified()
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -1474,7 +1362,7 @@ class TestOnlineAccount:
lp.sec("ac2: read message and check it's verified chat")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert msg.chat.is_protected()
assert msg.chat.is_verified()
assert msg.is_encrypted()
lp.sec("ac2: send message and let ac1 read it")
@@ -1609,7 +1497,7 @@ class TestOnlineAccount:
lp.sec("ac1 blocks ac2")
contact = ac1.create_contact(ac2)
contact.block()
contact.set_blocked()
assert contact.is_blocked()
ev = ac1._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
assert ev.data1 == contact.id
@@ -2063,7 +1951,6 @@ class TestOnlineAccount:
assert ac1.direct_imap.idle_wait_for_seen()
ac1_clone = acfactory.clone_online_account(ac1)
ac1_clone.set_config("fetch_existing_msgs", "1")
ac1_clone._configtracker.wait_finish()
ac1_clone.start_io()
@@ -2197,7 +2084,6 @@ class TestOnlineConfigureFails:
ac1, configdict = acfactory.get_online_config()
ac1.update_config(dict(addr=configdict["addr"], mail_pw="123"))
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
@@ -2205,7 +2091,6 @@ class TestOnlineConfigureFails:
ac1, configdict = acfactory.get_online_config()
ac1.update_config(dict(addr="x" + configdict["addr"], mail_pw=configdict["mail_pw"]))
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
@@ -2213,6 +2098,5 @@ class TestOnlineConfigureFails:
ac1, configdict = acfactory.get_online_config()
ac1.update_config((dict(addr=configdict["addr"] + "x", mail_pw=configdict["mail_pw"])))
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")

View File

@@ -63,14 +63,13 @@ def main():
ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml")
assert core_toml == ffi_toml, (core_toml, ffi_toml)
if "alpha" not in newversion:
for line in open("CHANGELOG.md"):
## 1.25.0
if line.startswith("## "):
if line[2:].strip().startswith(newversion):
break
else:
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
for line in open("CHANGELOG.md"):
## 1.25.0
if line.startswith("## "):
if line[2:].strip().startswith(newversion):
break
else:
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
replace_toml_version("Cargo.toml", newversion)
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)

View File

@@ -32,7 +32,7 @@ Messages SHOULD be encrypted by the
`prefer-encrypt=mutual` MAY be set by default.
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
by the [Protected Headers](https://tools.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
by the [Protected Headers](https://www.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
# Outgoing messages

View File

@@ -188,7 +188,7 @@ impl Accounts {
let id = self.add_account().await?;
let ctx = self.get_account(id).await.expect("just added");
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, &file).await {
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, Some(file)).await {
Ok(_) => Ok(id),
Err(err) => {
// remove temp account
@@ -347,6 +347,7 @@ impl Config {
inner.accounts.push(AccountConfig {
id,
name: String::new(),
dir: target_dir.into(),
uuid,
});
@@ -413,6 +414,8 @@ impl Config {
pub struct AccountConfig {
/// Unique id.
pub id: u32,
/// Display name
pub name: String,
/// Root directory for all data for this account.
pub dir: std::path::PathBuf,
pub uuid: Uuid,

View File

@@ -63,12 +63,6 @@ impl<'a> BlobObject<'a> {
blobname: name.clone(),
cause: err.into(),
})?;
// workaround a bug in async-std
// (the executor does not handle blocking operation in Drop correctly,
// see https://github.com/async-rs/async-std/issues/900 )
let _ = file.flush().await;
let blob = BlobObject {
blobdir,
name: format!("$BLOBDIR/{}", name),
@@ -157,10 +151,6 @@ impl<'a> BlobObject<'a> {
cause: err,
});
}
// workaround, see create() for details
let _ = dst_file.flush().await;
let blob = BlobObject {
blobdir: context.get_blobdir(),
name: format!("$BLOBDIR/{}", name),

View File

@@ -1,6 +1,5 @@
//! # Chat module
use deltachat_derive::{FromSql, ToSql};
use std::convert::TryFrom;
use std::time::{Duration, SystemTime};
@@ -46,33 +45,6 @@ pub enum ChatItem {
},
}
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum ProtectionStatus {
Unprotected = 0,
Protected = 1,
}
impl Default for ProtectionStatus {
fn default() -> Self {
ProtectionStatus::Unprotected
}
}
/// Chat ID, including reserved IDs.
///
/// Some chat IDs are reserved to identify special chat types. This
@@ -174,109 +146,6 @@ impl ChatId {
self.set_blocked(context, Blocked::Not).await;
}
/// Sets protection without sending a message.
///
/// Used when a message arrives indicating that someone else has
/// changed the protection value for a chat.
pub(crate) async fn inner_set_protection(
self,
context: &Context,
protect: ProtectionStatus,
) -> Result<(), Error> {
ensure!(!self.is_special(), "Invalid chat-id.");
let chat = Chat::load_from_db(context, self).await?;
if protect == chat.protected {
info!(context, "Protection status unchanged for {}.", self);
return Ok(());
}
match protect {
ProtectionStatus::Protected => match chat.typ {
Chattype::Single | Chattype::Group => {
let contact_ids = get_chat_contacts(context, self).await;
for contact_id in contact_ids.into_iter() {
let contact = Contact::get_by_id(context, contact_id).await?;
if contact.is_verified(context).await != VerifiedStatus::BidirectVerified {
bail!("{} is not verified.", contact.get_display_name());
}
}
}
Chattype::Undefined => bail!("Undefined group type"),
},
ProtectionStatus::Unprotected => {}
};
context
.sql
.execute(
"UPDATE chats SET protected=? WHERE id=?;",
paramsv![protect, self],
)
.await?;
context.emit_event(EventType::ChatModified(self));
// make sure, the receivers will get all keys
reset_gossiped_timestamp(context, self).await?;
Ok(())
}
/// Send protected status message to the chat.
///
/// This sends the message with the protected status change to the chat,
/// notifying the user on this device as well as the other users in the chat.
///
/// If `promote` is false this means, the message must not be sent out
/// and only a local info message should be added to the chat.
/// This is used when protection is enabled implicitly or when a chat is not yet promoted.
pub(crate) async fn add_protection_msg(
self,
context: &Context,
protect: ProtectionStatus,
promote: bool,
from_id: u32,
) -> Result<(), Error> {
let msg_text = context.stock_protection_msg(protect, from_id).await;
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
};
if promote {
let mut msg = Message::default();
msg.viewtype = Viewtype::Text;
msg.text = Some(msg_text);
msg.param.set_cmd(cmd);
send_msg(context, self, &mut msg).await?;
} else {
add_info_msg_with_cmd(context, self, msg_text, cmd).await?;
}
Ok(())
}
/// Sets protection and sends or adds a message.
pub async fn set_protection(
self,
context: &Context,
protect: ProtectionStatus,
) -> Result<(), Error> {
ensure!(!self.is_special(), "set protection: invalid chat-id.");
let chat = Chat::load_from_db(context, self).await?;
if let Err(e) = self.inner_set_protection(context, protect).await {
error!(context, "Cannot set protection: {}", e); // make error user-visible
return Err(e);
}
self.add_protection_msg(context, protect, chat.is_promoted(), DC_CONTACT_ID_SELF)
.await
}
/// Archives or unarchives a chat.
pub async fn set_visibility(
self,
@@ -337,7 +206,7 @@ impl ChatId {
);
/* Up to 2017-11-02 deleting a group also implied leaving it, see above why we have changed this. */
let chat = Chat::load_from_db(context, self).await?;
let _chat = Chat::load_from_db(context, self).await?;
context
.sql
.execute(
@@ -373,17 +242,6 @@ impl ChatId {
let j = job::Job::new(Action::Housekeeping, 0, Params::new(), 10);
job::add(context, j).await;
if chat.is_self_talk() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(
context
.stock_str(StockMessage::SelfDeletedMsgBody)
.await
.into(),
);
add_device_msg(&context, None, Some(&mut msg)).await?;
}
Ok(())
}
@@ -471,12 +329,6 @@ impl ChatId {
msg.param.set(Param::File, blob.as_name());
}
}
let chat = Chat::load_from_db(context, self).await?;
if !chat.can_send() {
bail!("Can't set a draft: Can't send");
}
context
.sql
.execute(
@@ -686,7 +538,6 @@ pub struct Chat {
pub param: Params,
is_sending_locations: bool,
pub mute_duration: MuteDuration,
protected: ProtectionStatus,
}
impl Chat {
@@ -696,7 +547,7 @@ impl Chat {
.sql
.query_row(
"SELECT c.type, c.name, c.grpid, c.param, c.archived,
c.blocked, c.locations_send_until, c.muted_until, c.protected
c.blocked, c.locations_send_until, c.muted_until
FROM chats c
WHERE c.id=?;",
paramsv![chat_id],
@@ -711,7 +562,6 @@ impl Chat {
blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(),
is_sending_locations: row.get(6)?,
mute_duration: row.get(7)?,
protected: row.get(8)?,
};
Ok(c)
},
@@ -877,9 +727,9 @@ impl Chat {
!self.is_unpromoted()
}
/// Returns true if chat protection is enabled.
pub fn is_protected(&self) -> bool {
self.protected == ProtectionStatus::Protected
/// Returns true if chat is a verified group chat.
pub fn is_verified(&self) -> bool {
self.typ == Chattype::VerifiedGroup
}
/// Returns true if location streaming is enabled in the chat.
@@ -906,12 +756,15 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
if !(self.typ == Chattype::Single || self.typ == Chattype::Group) {
if !(self.typ == Chattype::Single
|| self.typ == Chattype::Group
|| self.typ == Chattype::VerifiedGroup)
{
error!(context, "Cannot send to chat type #{}.", self.typ,);
bail!("Cannot set to chat type #{}", self.typ);
}
if self.typ == Chattype::Group
if (self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup)
&& !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await
{
emit_event!(
@@ -928,7 +781,7 @@ impl Chat {
let new_rfc724_mid = {
let grpid = match self.typ {
Chattype::Group => Some(self.grpid.as_str()),
Chattype::Group | Chattype::VerifiedGroup => Some(self.grpid.as_str()),
_ => None,
};
dc_create_outgoing_rfc724_mid(grpid, &from)
@@ -952,7 +805,7 @@ impl Chat {
);
bail!("Cannot set message, contact for {} not found.", self.id);
}
} else if self.typ == Chattype::Group
} else if (self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup)
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
{
msg.param.set_int(Param::AttachGroupImage, 1);
@@ -1147,7 +1000,7 @@ pub struct ChatInfo {
///
/// On the C API this number is one of the
/// `DC_CHAT_TYPE_UNDEFINED`, `DC_CHAT_TYPE_SINGLE`,
/// or `DC_CHAT_TYPE_GROUP`
/// `DC_CHAT_TYPE_GROUP` or `DC_CHAT_TYPE_VERIFIED_GROUP`
/// constants.
#[serde(rename = "type")]
pub type_: u32,
@@ -1834,34 +1687,12 @@ pub async fn get_chat_msgs(
}
}
pub(crate) async fn marknoticed_chat_if_older_than(
context: &Context,
chat_id: ChatId,
timestamp: i64,
) -> Result<(), Error> {
if let Some(chat_timestamp) = context
.sql
.query_get_value(
context,
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
paramsv![chat_id],
)
.await
{
if timestamp > chat_timestamp {
marknoticed_chat(context, chat_id).await?;
}
}
Ok(())
}
pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<(), Error> {
// "WHERE" below uses the index `(state, hidden, chat_id)`, see get_fresh_msg_cnt() for reasoning
if !context
.sql
.exists(
"SELECT id FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;",
paramsv![MessageState::InFresh, chat_id],
"SELECT id FROM msgs WHERE chat_id=? AND state=?;",
paramsv![chat_id, MessageState::InFresh],
)
.await?
{
@@ -1872,11 +1703,10 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<(),
.sql
.execute(
"UPDATE msgs
SET state=?
WHERE state=?
AND hidden=0
AND chat_id=?;",
paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id],
SET state=13
WHERE chat_id=?
AND state=10;",
paramsv![chat_id],
)
.await?;
@@ -2012,7 +1842,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Vec<u32> {
pub async fn create_group_chat(
context: &Context,
protect: ProtectionStatus,
verified: VerifiedStatus,
chat_name: impl AsRef<str>,
) -> Result<ChatId, Error> {
let chat_name = improve_single_line_input(chat_name);
@@ -2026,7 +1856,11 @@ pub async fn create_group_chat(
context.sql.execute(
"INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);",
paramsv![
Chattype::Group,
if verified != VerifiedStatus::Unverified {
Chattype::VerifiedGroup
} else {
Chattype::Group
},
chat_name,
grpid,
time(),
@@ -2050,12 +1884,6 @@ pub async fn create_group_chat(
chat_id: ChatId::new(0),
});
if protect == ProtectionStatus::Protected {
// this part is to stay compatible to verified groups,
// in some future, we will drop the "protect"-flag from create_group_chat()
chat_id.inner_set_protection(context, protect).await?;
}
Ok(chat_id)
}
@@ -2181,12 +2009,12 @@ pub(crate) async fn add_contact_to_chat_ex(
}
} else {
// else continue and send status mail
if chat.is_protected()
if chat.typ == Chattype::VerifiedGroup
&& contact.is_verified(context).await != VerifiedStatus::BidirectVerified
{
error!(
context,
"Only bidirectional verified contacts can be added to protected chats."
"Only bidirectional verified contacts can be added to verified groups."
);
return Ok(false);
}
@@ -2224,7 +2052,7 @@ async fn real_group_exists(context: &Context, chat_id: ChatId) -> bool {
context
.sql
.exists(
"SELECT id FROM chats WHERE id=? AND type=120;",
"SELECT id FROM chats WHERE id=? AND (type=120 OR type=130);",
paramsv![chat_id],
)
.await
@@ -2750,7 +2578,7 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> usize {
}
}
/// Returns a tuple of `(chatid, is_protected, blocked)`.
/// Returns a tuple of `(chatid, is_verified, blocked)`.
pub(crate) async fn get_chat_id_by_grpid(
context: &Context,
grpid: impl AsRef<str>,
@@ -2758,16 +2586,14 @@ pub(crate) async fn get_chat_id_by_grpid(
context
.sql
.query_row(
"SELECT id, blocked, protected FROM chats WHERE grpid=?;",
"SELECT id, blocked, type FROM chats WHERE grpid=?;",
paramsv![grpid.as_ref()],
|row| {
let chat_id = row.get::<_, ChatId>(0)?;
let b = row.get::<_, Option<Blocked>>(1)?.unwrap_or_default();
let p = row
.get::<_, Option<ProtectionStatus>>(2)?
.unwrap_or_default();
Ok((chat_id, p == ProtectionStatus::Protected, b))
let v = row.get::<_, Option<Chattype>>(2)?.unwrap_or_default();
Ok((chat_id, v == Chattype::VerifiedGroup, b))
},
)
.await
@@ -2920,22 +2746,18 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
/// Adds an informational message to chat.
///
/// For example, it can be a message showing that a member was added to a group.
pub(crate) async fn add_info_msg_with_cmd(
context: &Context,
chat_id: ChatId,
text: impl AsRef<str>,
cmd: SystemMessage,
) -> Result<MsgId, Error> {
pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef<str>) {
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
let ephemeral_timer = match chat_id.get_ephemeral_timer(context).await {
Err(e) => {
warn!(context, "Could not get timer for info msg: {}", e);
return;
}
Ok(ephemeral_timer) => ephemeral_timer,
};
let mut param = Params::new();
if cmd != SystemMessage::Unknown {
param.set_cmd(cmd)
}
context.sql.execute(
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid,ephemeral_timer, param) VALUES (?,?,?, ?,?,?, ?,?,?, ?);",
if let Err(e) = context.sql.execute(
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid,ephemeral_timer) VALUES (?,?,?, ?,?,?, ?,?,?);",
paramsv![
chat_id,
DC_CONTACT_ID_INFO,
@@ -2945,25 +2767,22 @@ pub(crate) async fn add_info_msg_with_cmd(
MessageState::InNoticed,
text.as_ref().to_string(),
rfc724_mid,
ephemeral_timer,
param.to_string(),
ephemeral_timer
]
).await?;
).await {
warn!(context, "Could not add info msg: {}", e);
return;
}
let row_id = context
.sql
.get_rowid(context, "msgs", "rfc724_mid", &rfc724_mid)
.await
.unwrap_or_default();
let msg_id = MsgId::new(row_id);
context.emit_event(EventType::MsgsChanged { chat_id, msg_id });
Ok(msg_id)
}
pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef<str>) {
if let Err(e) = add_info_msg_with_cmd(context, chat_id, text, SystemMessage::Unknown).await {
warn!(context, "Could not add info msg: {}", e);
}
context.emit_event(EventType::MsgsChanged {
chat_id,
msg_id: MsgId::new(row_id),
});
}
#[cfg(test)]
@@ -3056,7 +2875,7 @@ mod tests {
async fn test_add_contact_to_chat_ex_add_self() {
// Adding self to a contact should succeed, even though it's pointless.
let t = TestContext::new().await;
let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
.await
.unwrap();
let added = add_contact_to_chat_ex(&t.ctx, chat_id, DC_CONTACT_ID_SELF, false)
@@ -3442,7 +3261,7 @@ mod tests {
.await
.unwrap();
async_std::task::sleep(std::time::Duration::from_millis(1000)).await;
let chat_id3 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
.await
.unwrap();
@@ -3487,7 +3306,7 @@ mod tests {
#[async_std::test]
async fn test_set_chat_name() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
.await
.unwrap();
assert_eq!(
@@ -3530,7 +3349,7 @@ mod tests {
#[async_std::test]
async fn test_shall_attach_selfavatar() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
.await
.unwrap();
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).await.unwrap());
@@ -3554,7 +3373,7 @@ mod tests {
#[async_std::test]
async fn test_set_mute_duration() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
.await
.unwrap();
// Initial
@@ -3618,116 +3437,4 @@ mod tests {
false
);
}
#[async_std::test]
async fn test_add_info_msg() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
add_info_msg(&t.ctx, chat_id, "foo info").await;
let msg = t.get_last_msg(chat_id).await;
assert_eq!(msg.get_chat_id(), chat_id);
assert_eq!(msg.get_viewtype(), Viewtype::Text);
assert_eq!(msg.get_text().unwrap(), "foo info");
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::Unknown);
}
#[async_std::test]
async fn test_add_info_msg_with_cmd() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
let msg_id = add_info_msg_with_cmd(
&t.ctx,
chat_id,
"foo bar info",
SystemMessage::EphemeralTimerChanged,
)
.await
.unwrap();
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
assert_eq!(msg.get_chat_id(), chat_id);
assert_eq!(msg.get_viewtype(), Viewtype::Text);
assert_eq!(msg.get_text().unwrap(), "foo bar info");
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::EphemeralTimerChanged);
let msg2 = t.get_last_msg(chat_id).await;
assert_eq!(msg.get_id(), msg2.get_id());
}
#[async_std::test]
async fn test_set_protection() {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
// enable protection on unpromoted chat, the info-message is added via add_info_msg()
chat_id
.set_protection(&t.ctx, ProtectionStatus::Protected)
.await
.unwrap();
let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
assert!(chat.is_protected());
assert!(chat.is_unpromoted());
let msgs = get_chat_msgs(&t.ctx, chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
let msg = t.get_last_msg(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::InNoticed);
// disable protection again, still unpromoted
chat_id
.set_protection(&t.ctx, ProtectionStatus::Unprotected)
.await
.unwrap();
let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
let msg = t.get_last_msg(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionDisabled);
assert_eq!(msg.get_state(), MessageState::InNoticed);
// send a message, this switches to promoted state
send_text_msg(&t.ctx, chat_id, "hi!".to_string())
.await
.unwrap();
let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
assert!(!chat.is_protected());
assert!(!chat.is_unpromoted());
let msgs = get_chat_msgs(&t.ctx, chat_id, 0, None).await;
assert_eq!(msgs.len(), 3);
// enable protection on promoted chat, the info-message is sent via send_msg() this time
chat_id
.set_protection(&t.ctx, ProtectionStatus::Protected)
.await
.unwrap();
let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
assert!(chat.is_protected());
assert!(!chat.is_unpromoted());
let msg = t.get_last_msg(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
}
}

View File

@@ -362,7 +362,9 @@ impl Chatlist {
let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
if lastmsg.from_id != DC_CONTACT_ID_SELF && chat.typ == Chattype::Group {
if lastmsg.from_id != DC_CONTACT_ID_SELF
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok();
}
@@ -438,13 +440,13 @@ mod tests {
#[async_std::test]
async fn test_try_load() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
let chat_id2 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "b chat")
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat")
.await
.unwrap();
let chat_id3 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "c chat")
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat")
.await
.unwrap();
@@ -487,7 +489,7 @@ mod tests {
async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new().await;
t.ctx.update_device_chats().await.unwrap();
create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
@@ -544,7 +546,7 @@ mod tests {
#[async_std::test]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();

View File

@@ -69,13 +69,6 @@ pub enum Config {
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
MediaQuality,
/// If set to "1", on the first time `start_io()` is called after configuring,
/// the newest existing messages are fetched.
/// Existing recipients are added to the contact database regardless of this setting.
#[strum(props(default = "0"))]
// disabled for now, we'll set this back to "1" at some point
FetchExistingMsgs,
#[strum(props(default = "0"))]
KeyGenType,
@@ -128,8 +121,6 @@ pub enum Config {
#[strum(serialize = "sys.config_keys")]
SysConfigKeys,
Bot,
/// Whether we send a warning if the password is wrong (set to false when we send a warning
/// because we do not want to send a second warning)
#[strum(props(default = "0"))]
@@ -253,16 +244,6 @@ impl Context {
job::schedule_resync(self).await;
ret
}
Config::InboxWatch => {
if self.get_config(Config::InboxWatch).await.as_deref() != value {
// If Inbox-watch is disabled and enabled again, do not fetch emails from in between.
// this avoids unexpected mass-downloads and -deletions (if delete_server_after is set)
if let Some(inbox) = self.get_config(Config::ConfiguredInboxFolder).await {
crate::imap::set_config_last_seen_uid(self, inbox, 0, 0).await;
}
}
self.sql.set_raw_config(self, key, value).await
}
_ => self.sql.set_raw_config(self, key, value).await,
}
}

View File

@@ -5,46 +5,31 @@ mod auto_outlook;
mod read_url;
mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
use async_std::prelude::*;
use async_std::task;
use itertools::Itertools;
use job::Action;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::imap::Imap;
use crate::job;
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::message::Message;
use crate::oauth2::*;
use crate::param::Params;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::stock::StockMessage;
use crate::EventType;
use crate::{chat, e2ee, provider};
use crate::{constants::*, job};
use crate::{context::Context, param::Params};
use anyhow::{bail, ensure, Context as _, Result};
use async_std::prelude::*;
use async_std::task;
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use itertools::Itertools;
use job::Action;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use server_params::{expand_param_vector, ServerParams};
macro_rules! progress {
($context:tt, $progress:expr, $comment: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);
};
}
impl Context {
/// Checks if the context is already configured.
pub async fn is_configured(&self) -> bool {
@@ -65,10 +50,18 @@ impl Context {
);
let cancel_channel = self.alloc_ongoing().await?;
let ctx2 = self.clone();
let progress = ProgressHandler::new(15.0, move |p| {
ctx2.emit_event(EventType::ConfigureProgress {
progress: p,
comment: None,
});
});
let res = self
.inner_configure()
.inner_configure(&progress)
.race(cancel_channel.recv().map(|_| {
progress!(self, 0);
progress.p(0);
Ok(())
}))
.await;
@@ -78,11 +71,11 @@ impl Context {
res
}
async fn inner_configure(&self) -> Result<()> {
async fn inner_configure(&self, progress: &impl Progress) -> Result<()> {
info!(self, "Configure ...");
let mut param = LoginParam::from_database(self, "").await;
let success = configure(self, &mut param).await;
let success = configure(self, &mut param, progress).await;
self.set_config(Config::NotifyAboutWrongPw, None).await?;
if let Some(provider) = provider::get_provider_info(&param.addr) {
@@ -116,21 +109,24 @@ impl Context {
Ok(_) => {
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
.await?;
progress!(self, 1000);
progress.p(1000);
Ok(())
}
Err(err) => {
progress!(
progress.kill().await;
emit_event!(
self,
0,
Some(
self.stock_string_repl_str(
StockMessage::ConfigurationFailed,
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
format!("{:#}", err),
EventType::ConfigureProgress {
progress: 0,
comment: Some(
self.stock_string_repl_str(
StockMessage::ConfigurationFailed,
// We are using Anyhow's .context() and to show the inner error too, we need the {:#}:
format!("{:#}", err),
)
.await
)
.await
)
}
);
Err(err)
}
@@ -138,8 +134,8 @@ impl Context {
}
}
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 1);
async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progress) -> Result<()> {
progress.p(1);
// Check basic settings.
ensure!(!param.addr.is_empty(), "Please enter an email address.");
@@ -163,15 +159,12 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
DC_LP_AUTH_NORMAL as i32
};
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
// Step 1: Load the parameters and check email-address and password
if oauth2 {
// the used oauth2 addr may differ, check this.
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
progress.p(10);
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, &param.addr, &param.imap.password)
.await
.and_then(|e| e.parse().ok())
@@ -182,7 +175,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
.set_raw_config(ctx, "addr", Some(param.addr.as_str()))
.await?;
}
progress!(ctx, 20);
progress.p(20);
}
// no oauth? - just continue it's no error
@@ -191,7 +184,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let param_addr_urlencoded = utf8_percent_encode(&param.addr, NON_ALPHANUMERIC).to_string();
// Step 2: Autoconfig
progress!(ctx, 200);
progress.p(200);
let param_autoconfig;
if param.imap.server.is_empty()
@@ -209,42 +202,38 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
param_autoconfig = Some(servers);
} else {
param_autoconfig =
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await;
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded, progress).await;
}
} else {
param_autoconfig = None;
}
progress!(ctx, 500);
progress.p(500);
let mut servers = param_autoconfig.unwrap_or_default();
if !servers
.iter()
.any(|server| server.protocol == Protocol::IMAP)
{
servers.push(ServerParams {
protocol: Protocol::IMAP,
hostname: param.imap.server.clone(),
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
})
}
if !servers
.iter()
.any(|server| server.protocol == Protocol::SMTP)
{
servers.push(ServerParams {
protocol: Protocol::SMTP,
hostname: param.smtp.server.clone(),
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
})
}
let servers = expand_param_vector(servers, &param.addr, &param_domain);
let servers = expand_param_vector(
param_autoconfig.unwrap_or_else(|| {
vec![
ServerParams {
protocol: Protocol::IMAP,
hostname: param.imap.server.clone(),
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
},
ServerParams {
protocol: Protocol::SMTP,
hostname: param.smtp.server.clone(),
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
},
]
}),
&param.addr,
&param_domain,
);
progress!(ctx, 550);
progress.p(550);
// Spawn SMTP configuration task
let mut smtp = Smtp::new();
@@ -285,7 +274,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
});
progress!(ctx, 600);
progress.p(600);
// Configure IMAP
let (_s, r) = async_std::sync::channel(1);
@@ -311,16 +300,13 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
Err(e) => errors.push(e),
}
progress!(
ctx,
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
progress.p(600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count);
}
if !imap_configured {
bail!(nicer_configuration_error(ctx, errors).await);
}
progress!(ctx, 850);
progress.p(850);
// Wait for SMTP configuration
match smtp_config_task.await {
@@ -332,7 +318,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
}
progress!(ctx, 900);
progress.p(900);
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
|| ctx.get_config_bool(Config::MvboxMove).await;
@@ -345,14 +331,14 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
drop(imap);
progress!(ctx, 910);
progress.p(910);
// configuration success - write back the configured parameters with the
// "configured_" prefix; also write the "configured"-flag */
// the trailing underscore is correct
param.save_to_database(ctx, "configured_").await?;
ctx.sql.set_raw_config_bool(ctx, "configured", true).await?;
progress!(ctx, 920);
progress.p(920);
e2ee::ensure_secret_key_exists(ctx).await?;
info!(ctx, "key generation completed");
@@ -363,8 +349,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
)
.await;
progress!(ctx, 940);
update_device_chats_handle.await?;
progress.p(940);
Ok(())
}
@@ -438,14 +423,15 @@ async fn get_autoconfig(
param: &LoginParam,
param_domain: &str,
param_addr_urlencoded: &str,
progress: &impl Progress,
) -> Option<Vec<ServerParams>> {
let sources = AutoconfigSource::all(param_domain, param_addr_urlencoded);
let mut progress = 300;
let mut p = 300;
for source in &sources {
let res = source.fetch(ctx, param).await;
progress!(ctx, progress);
progress += 10;
progress.p(p);
p += 10;
if let Ok(res) = res {
return Some(res);
}
@@ -564,9 +550,7 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
let first_err = if let Some(f) = errors.first() {
f
} else {
// This means configuration failed but no errors have been captured. This should never
// happen, but if it does, the user will see classic "Error: no error".
return "no error".to_string();
return "".to_string();
};
if errors

View File

@@ -1,9 +1,11 @@
//! # Constants
use deltachat_derive::*;
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
lazy_static! {
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
}
#[derive(
Debug,
@@ -143,6 +145,7 @@ pub enum Chattype {
Undefined = 0,
Single = 100,
Group = 120,
VerifiedGroup = 130,
}
impl Default for Chattype {
@@ -205,11 +208,6 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is splitted to chunks.
// this does not affect MIME'e `To:` header.
// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db.
pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
#[derive(
Debug,
Display,

View File

@@ -3,7 +3,7 @@
use async_std::path::PathBuf;
use deltachat_derive::*;
use itertools::Itertools;
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
use regex::Regex;
use crate::aheader::EncryptPreference;
@@ -1053,7 +1053,9 @@ pub fn addr_normalize(addr: &str) -> &str {
}
fn sanitize_name_and_addr(name: impl AsRef<str>, addr: impl AsRef<str>) -> (String, String) {
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
lazy_static! {
static ref ADDR_WITH_NAME_REGEX: Regex = Regex::new("(.*)<(.*)>").unwrap();
}
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.as_ref().is_empty() {

View File

@@ -163,6 +163,10 @@ impl Context {
/// Stops the IO scheduler.
pub async fn stop_io(&self) {
info!(self, "stopping IO");
if !self.is_io_running().await {
info!(self, "IO is not running");
return;
}
self.inner.stop_io().await;
}
@@ -479,19 +483,14 @@ impl InnerContext {
}
async fn stop_io(&self) {
if self.is_io_running().await {
let token = {
let lock = &*self.scheduler.read().await;
lock.pre_stop().await
};
{
let lock = &mut *self.scheduler.write().await;
lock.stop(token).await;
}
}
if let Some(ephemeral_task) = self.ephemeral_task.write().await.take() {
ephemeral_task.cancel().await;
assert!(self.is_io_running().await, "context is already stopped");
let token = {
let lock = &*self.scheduler.read().await;
lock.pre_stop().await
};
{
let lock = &mut *self.scheduler.write().await;
lock.stop(token).await;
}
}
}

View File

@@ -4,7 +4,7 @@ use sha2::{Digest, Sha256};
use mailparse::SingleInfo;
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::constants::*;
use crate::contact::*;
@@ -387,7 +387,6 @@ async fn add_parts(
// this message is a classic email not a chat-message nor a reply to one
match show_emails {
ShowEmails::Off => {
info!(context, "Classical email not shown (TRASH)");
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
allow_creation = false;
}
@@ -446,7 +445,10 @@ async fn add_parts(
// it might also be blocked and displayed in the deaddrop as a result
if chat_id.is_unset() && mime_parser.failure_report.is_some() {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
info!(context, "Message belongs to an NDN (TRASH)",);
info!(
context,
"Message belongs to an NDN and is not shown in a chat.",
);
}
if chat_id.is_unset() {
@@ -487,7 +489,7 @@ async fn add_parts(
// check if the message belongs to a mailing list
if mime_parser.is_mailinglist_message() {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
info!(context, "Message belongs to a mailing list (TRASH)");
info!(context, "Message belongs to a mailing list and is ignored.",);
}
}
@@ -531,7 +533,6 @@ async fn add_parts(
if chat_id.is_unset() {
// maybe from_id is null or sth. else is suspicious, move message to trash
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
info!(context, "No chat id for incoming msg (TRASH)")
}
// if the chat_id is blocked,
@@ -641,14 +642,13 @@ async fn add_parts(
}
if chat_id.is_unset() {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
info!(context, "No chat id for outgoing message (TRASH)")
}
}
if fetching_existing_messages && mime_parser.decrypting_failed {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
// We are only gathering old messages on first start. We do not want to add loads of non-decryptable messages to the chats.
info!(context, "Existing non-decipherable message. (TRASH)");
info!(context, "Dropping existing non-decipherable message.");
}
// Extract ephemeral timer from the message.
@@ -714,43 +714,6 @@ async fn add_parts(
ephemeral_timer = EphemeralTimer::Disabled;
}
// if a chat is protected, check additional properties
if !chat_id.is_special() {
let chat = Chat::load_from_db(context, *chat_id).await?;
let new_status = match mime_parser.is_system_message {
SystemMessage::ChatProtectionEnabled => Some(ProtectionStatus::Protected),
SystemMessage::ChatProtectionDisabled => Some(ProtectionStatus::Unprotected),
_ => None,
};
if chat.is_protected() || new_status.is_some() {
if let Err(err) =
check_verified_properties(context, mime_parser, from_id as u32, to_ids).await
{
warn!(context, "verification problem: {}", err);
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(s);
} else {
// change chat protection only when verification check passes
if let Some(new_status) = new_status {
if let Err(e) = chat_id.inner_set_protection(context, new_status).await {
chat::add_info_msg(
context,
*chat_id,
format!("Cannot set protection: {}", e),
)
.await;
return Ok(()); // do not return an error as this would result in retrying the message
}
set_better_msg(
mime_parser,
context.stock_protection_msg(new_status, from_id).await,
);
}
}
}
}
// correct message_timestamp, it should not be used before,
// however, we cannot do this earlier as we need from_id to be set
let in_fresh = state == MessageState::InFresh;
@@ -772,6 +735,9 @@ async fn add_parts(
*sent_timestamp = std::cmp::min(*sent_timestamp, rcvd_timestamp);
// unarchive chat
chat_id.unarchive(context).await?;
// if the mime-headers should be saved, find out its size
// (the mime-header ends with an empty line)
let save_mime_headers = context.get_config_bool(Config::SaveMimeHeaders).await;
@@ -893,10 +859,6 @@ async fn add_parts(
*insert_msg_id = *id;
}
if !is_hidden {
chat_id.unarchive(context).await?;
}
*hidden = is_hidden;
created_db_entries.extend(ids.iter().map(|id| (chat_id, *id)));
mime_parser.parts = new_parts;
@@ -906,11 +868,6 @@ async fn add_parts(
"Message has {} parts and is assigned to chat #{}.", icnt, chat_id,
);
// new outgoing message from another device marks the chat as noticed.
if !incoming && !*hidden && !chat_id.is_special() {
chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?;
}
// check event to send
if chat_id.is_trash() || *hidden {
*create_event_to_send = None;
@@ -1080,24 +1037,37 @@ async fn create_or_lookup_group(
set_better_msg(mime_parser, &better_msg);
}
let grpid = try_getting_grpid(mime_parser);
if grpid.is_empty() {
return create_or_lookup_adhoc_group(
context,
mime_parser,
allow_creation,
create_blocked,
from_id,
to_ids,
)
.await
.map_err(|err| {
info!(context, "could not create adhoc-group: {:?}", err);
err
});
let mut grpid = "".to_string();
if let Some(optional_field) = mime_parser.get(HeaderDef::ChatGroupId) {
grpid = optional_field.clone();
}
if grpid.is_empty() {
if let Some(extracted_grpid) = mime_parser
.get(HeaderDef::MessageId)
.and_then(|value| dc_extract_grpid_from_rfc724_mid(&value))
{
grpid = extracted_grpid.to_string();
} else if let Some(extracted_grpid) = extract_grpid(mime_parser, HeaderDef::InReplyTo) {
grpid = extracted_grpid.to_string();
} else if let Some(extracted_grpid) = extract_grpid(mime_parser, HeaderDef::References) {
grpid = extracted_grpid.to_string();
} else {
return create_or_lookup_adhoc_group(
context,
mime_parser,
allow_creation,
create_blocked,
from_id,
to_ids,
)
.await
.map_err(|err| {
info!(context, "could not create adhoc-group: {:?}", err);
err
});
}
}
// now we have a grpid that is non-empty
// but we might not know about this group
@@ -1179,18 +1149,29 @@ async fn create_or_lookup_group(
set_better_msg(mime_parser, &better_msg);
// check, if we have a chat with this group ID
let (mut chat_id, _, _blocked) = chat::get_chat_id_by_grpid(context, &grpid)
let (mut chat_id, chat_id_verified, _blocked) = chat::get_chat_id_by_grpid(context, &grpid)
.await
.unwrap_or((ChatId::new(0), false, Blocked::Not));
if !chat_id.is_unset() && !chat::is_contact_in_chat(context, chat_id, from_id as u32).await {
// The From-address is not part of this group.
// It could be a new user or a DSN from a mailer-daemon.
// in any case we do not want to recreate the member list
// but still show the message as part of the chat.
// After all, the sender has a reference/in-reply-to that
// points to this chat.
let s = context.stock_str(StockMessage::UnknownSenderForChat).await;
mime_parser.repl_msg_by_error(s.to_string());
if !chat_id.is_unset() {
if chat_id_verified {
if let Err(err) =
check_verified_properties(context, mime_parser, from_id as u32, to_ids).await
{
warn!(context, "verification problem: {}", err);
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(s);
}
}
if !chat::is_contact_in_chat(context, chat_id, from_id as u32).await {
// The From-address is not part of this group.
// It could be a new user or a DSN from a mailer-daemon.
// in any case we do not want to recreate the member list
// but still show the message as part of the chat.
// After all, the sender has a reference/in-reply-to that
// points to this chat.
let s = context.stock_str(StockMessage::UnknownSenderForChat).await;
mime_parser.repl_msg_by_error(s.to_string());
}
}
// check if the group does not exist but should be created
@@ -1213,7 +1194,7 @@ async fn create_or_lookup_group(
|| X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
{
// group does not exist but should be created
let create_protected = if mime_parser.get(HeaderDef::ChatVerified).is_some() {
let create_verified = if mime_parser.get(HeaderDef::ChatVerified).is_some() {
if let Err(err) =
check_verified_properties(context, mime_parser, from_id as u32, to_ids).await
{
@@ -1221,9 +1202,9 @@ async fn create_or_lookup_group(
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(&s);
}
ProtectionStatus::Protected
VerifiedStatus::Verified
} else {
ProtectionStatus::Unprotected
VerifiedStatus::Unverified
};
if !allow_creation {
@@ -1236,22 +1217,11 @@ async fn create_or_lookup_group(
&grpid,
grpname.as_ref().unwrap(),
create_blocked,
create_protected,
create_verified,
)
.await;
chat_id_blocked = create_blocked;
recreate_member_list = true;
// once, we have protected-chats explained in UI, we can uncomment the following lines.
// ("verified groups" did not add a message anyway)
//
//if create_protected == ProtectionStatus::Protected {
// set from_id=0 as it is not clear that the sender of this random group message
// actually really has enabled chat-protection at some point.
//chat_id
// .add_protection_msg(context, ProtectionStatus::Protected, false, 0)
// .await?;
//}
}
// again, check chat_id
@@ -1267,7 +1237,6 @@ async fn create_or_lookup_group(
} else {
// The message was decrypted successfully, but contains a late "quit" or otherwise
// unwanted message.
info!(context, "message belongs to unwanted group (TRASH)");
return Ok((ChatId::new(DC_CHAT_ID_TRASH), chat_id_blocked));
}
}
@@ -1304,10 +1273,7 @@ async fn create_or_lookup_group(
}
}
}
} else if mime_parser.is_system_message == SystemMessage::ChatProtectionEnabled {
recreate_member_list = true;
}
if let Some(avatar_action) = &mime_parser.group_avatar {
info!(context, "group-avatar change for {}", chat_id);
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
@@ -1367,27 +1333,6 @@ async fn create_or_lookup_group(
Ok((chat_id, chat_id_blocked))
}
fn try_getting_grpid(mime_parser: &MimeMessage) -> String {
if let Some(optional_field) = mime_parser.get(HeaderDef::ChatGroupId) {
return optional_field.clone();
}
if let Some(extracted_grpid) = mime_parser
.get(HeaderDef::MessageId)
.and_then(|value| dc_extract_grpid_from_rfc724_mid(&value))
{
return extracted_grpid.to_string();
}
if !mime_parser.has_chat_version() {
if let Some(extracted_grpid) = extract_grpid(mime_parser, HeaderDef::InReplyTo) {
return extracted_grpid.to_string();
} else if let Some(extracted_grpid) = extract_grpid(mime_parser, HeaderDef::References) {
return extracted_grpid.to_string();
}
}
"".to_string()
}
/// try extract a grpid from a message-id list header value
fn extract_grpid(mime_parser: &MimeMessage, headerdef: HeaderDef) -> Option<&str> {
let header = mime_parser.get(headerdef)?;
@@ -1513,7 +1458,7 @@ async fn create_or_lookup_adhoc_group(
&grpid,
grpname,
create_blocked,
ProtectionStatus::Unprotected,
VerifiedStatus::Unverified,
)
.await;
for &member_id in &member_ids {
@@ -1530,17 +1475,20 @@ async fn create_group_record(
grpid: impl AsRef<str>,
grpname: impl AsRef<str>,
create_blocked: Blocked,
create_protected: ProtectionStatus,
create_verified: VerifiedStatus,
) -> ChatId {
if context.sql.execute(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected) VALUES(?, ?, ?, ?, ?, ?);",
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?);",
paramsv![
Chattype::Group,
if VerifiedStatus::Unverified != create_verified {
Chattype::VerifiedGroup
} else {
Chattype::Group
},
grpname.as_ref(),
grpid.as_ref(),
create_blocked,
time(),
create_protected,
],
).await
.is_err()
@@ -1692,17 +1640,6 @@ async fn check_verified_properties(
ensure!(mimeparser.was_encrypted(), "This message is not encrypted.");
if mimeparser.get(HeaderDef::ChatVerified).is_none() {
// we do not fail here currently, this would exclude (a) non-deltas
// and (b) deltas with different protection views across multiple devices.
// for group creation or protection enabled/disabled, however, Chat-Verified is respected.
warn!(
context,
"{} did not mark message as protected.",
contact.get_addr()
);
}
// ensure, the contact is verified
// and the message is signed with a verified key of the sender.
// this check is skipped for SELF as there is no proper SELF-peerstate
@@ -1792,7 +1729,7 @@ async fn check_verified_properties(
}
if !is_verified {
bail!(
"{} is not a member of this protected chat",
"{} is not a member of this verified group",
to_addr.to_string()
);
}
@@ -2228,7 +2165,7 @@ mod tests {
let t = TestContext::new_alice().await;
// create one-to-one with bob, archive one-to-one
let bob_id = Contact::create(&t.ctx, "bob", "bob@example.com")
let bob_id = Contact::create(&t.ctx, "bob", "bob@exampel.org")
.await
.unwrap();
let one2one_id = chat::create_by_contact_id(&t.ctx, bob_id).await.unwrap();
@@ -2240,7 +2177,7 @@ mod tests {
assert!(one2one.get_visibility() == ChatVisibility::Archived);
// create a group with bob, archive group
let group_id = chat::create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
let group_id = chat::create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
.await
.unwrap();
chat::add_contact_to_chat(&t.ctx, group_id, bob_id).await;

View File

@@ -2,15 +2,20 @@
//! no references to Context and other "larger" entities here.
use core::cmp::{max, min};
use std::borrow::Cow;
use std::fmt;
use std::io::Cursor;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use std::{borrow::Cow, sync::Arc};
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use async_std::{
path::{Path, PathBuf},
sync::Mutex,
task,
};
use async_trait::async_trait;
use chrono::{Local, TimeZone};
use rand::{thread_rng, Rng};
@@ -24,6 +29,94 @@ use crate::message::Message;
use crate::provider::get_provider_update_timestamp;
use crate::stock::StockMessage;
#[derive(Debug)]
pub struct ProgressHandlerInner<F: Fn(usize) + Send> {
progress_limit: usize,
emitted_progress: f64,
step_fraction: f64,
f: F,
}
#[derive(Debug)]
pub struct ProgressHandler<F: 'static + Fn(usize) + Send> {
inner: Arc<Mutex<ProgressHandlerInner<F>>>,
}
impl<F> ProgressHandler<F>
where
F: 'static + Fn(usize) + Send,
{
/// If step_fraction is e.g. 15, then every 100ms we will step by 1/15th of the remaining interval.
/// The bigger this value, the slower the progress bar will move in the beginning.
/// f is the function that is invoked when progress is made.
pub fn new(step_fraction: f64, f: F) -> Self {
let ret = Arc::new(Mutex::new(ProgressHandlerInner {
progress_limit: 1,
emitted_progress: 0f64,
step_fraction,
f,
}));
let cloned = ret.clone();
task::spawn(async move {
loop {
task::sleep(Duration::from_millis(100)).await;
{
let mut lock = cloned.lock().await;
let limit = lock.progress_limit;
if limit == 1000 || limit == 0 {
return;
}
let last = lock.emitted_progress;
let next = last + ((limit as f64 - last) / lock.step_fraction);
if (next / 10f64).ceil() - (last / 10f64).ceil() > 0f64 {
(lock.f)(next.ceil() as usize);
}
lock.emitted_progress = next;
drop(lock);
};
}
});
Self { inner: ret }
}
}
#[async_trait]
pub trait Progress {
/// Report actually made progress 0-1000 promille. The progress bar will slowly move toward the value set by this function.
/// Set rather high values as the progress bar will stay lower first,
/// i.e. don't start with values near 0 and end with values near 1000
fn p(&self, progress: usize);
/// Stops the progress handler without emitting any other events
async fn kill(&self);
}
#[async_trait]
impl<F> Progress for ProgressHandler<F>
where
F: 'static + Fn(usize) + Send,
{
fn p(&self, progress: usize) {
assert!(
progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
let inner = self.inner.clone();
task::spawn(async move {
if progress == 1000 || progress == 0 {
let inner = &inner.lock().await;
(inner.f)(progress);
}
inner.lock().await.progress_limit = progress;
});
}
async fn kill(&self) {
self.inner.lock().await.progress_limit = 0usize;
}
}
/// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string.
#[allow(clippy::indexing_slicing)]
@@ -717,7 +810,10 @@ where
T: AsRef<str>,
{
fn is_none_or_empty(&self) -> bool {
!matches!(self, Some(s) if !s.as_ref().is_empty())
match self {
Some(s) if !s.as_ref().is_empty() => false,
_ => true,
}
}
}
@@ -1174,31 +1270,22 @@ mod tests {
assert_eq!(msgs.len(), 1);
// do not repeat the warning every day ...
// (we test that for the 2 subsequent days, this may be the next month, so the result should be 1 or 2 device message)
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + (365 + 1) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + (365 + 2) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
let test_len = msgs.len();
assert!(test_len == 1 || test_len == 2);
assert_eq!(msgs.len(), 1);
// ... but every month
// (forward generous 33 days to avoid being in the same month as in the previous check)
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + (365 + 33) * 24 * 60 * 60,
timestamp_now + (365 + 31) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
@@ -1206,6 +1293,6 @@ mod tests {
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), test_len + 1);
assert_eq!(msgs.len(), 2);
}
}

View File

@@ -2,10 +2,12 @@
//!
//! A module to remove HTML tags from the email text
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
lazy_static! {
static ref LINE_RE: regex::Regex = regex::Regex::new(r"(\r?\n)+").unwrap();
}
struct Dehtml {
strbuilder: String,
@@ -22,16 +24,16 @@ enum AddText {
// dehtml() returns way too many newlines; however, an optimisation on this issue is not needed as
// the newlines are typically removed in further processing by the caller
pub fn dehtml(buf: &str) -> Option<String> {
pub fn dehtml(buf: &str) -> String {
let s = dehtml_quick_xml(buf);
if !s.trim().is_empty() {
return Some(s);
return s;
}
let s = dehtml_manually(buf);
if !s.trim().is_empty() {
return Some(s);
return s;
}
None
buf.to_string()
}
pub fn dehtml_quick_xml(buf: &str) -> String {
@@ -220,23 +222,21 @@ mod tests {
"<a href='https://get.delta.chat/'/>",
"[](https://get.delta.chat/)",
),
("", ""),
("<!doctype html>\n<b>fat text</b>", "*fat text*"),
// Invalid html (at least DC should show the text if the html is invalid):
("<!some invalid html code>\n<b>some text</b>", "some text"),
("<This text is in brackets>", "<This text is in brackets>"),
];
for (input, output) in cases {
assert_eq!(simplify(dehtml(input).unwrap(), true).0, output);
}
let none_cases = vec!["<html> </html>", ""];
for input in none_cases {
assert_eq!(dehtml(input), None);
assert_eq!(simplify(dehtml(input), true).0, output);
}
}
#[test]
fn test_dehtml_parse_br() {
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
let plain = dehtml(html).unwrap();
let plain = dehtml(html);
assert_eq!(plain, "line1\n\r\r\rline2\nline3");
}
@@ -244,7 +244,7 @@ mod tests {
#[test]
fn test_dehtml_parse_href() {
let html = "<a href=url>text</a";
let plain = dehtml(html).unwrap();
let plain = dehtml(html);
assert_eq!(plain, "[text](url)");
}
@@ -252,7 +252,7 @@ mod tests {
#[test]
fn test_dehtml_bold_text() {
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = dehtml(html).unwrap();
let plain = dehtml(html);
assert_eq!(plain, "text *bold*<>");
}
@@ -262,7 +262,7 @@ mod tests {
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let plain = dehtml(html).unwrap();
let plain = dehtml(html);
assert_eq!(
plain,
@@ -285,7 +285,7 @@ mod tests {
</body>
</html>
"##;
let txt = dehtml(input).unwrap();
let txt = dehtml(input);
assert_eq!(txt.trim(), "lots of text");
}
}

View File

@@ -57,9 +57,12 @@ impl EncryptHelper {
/// preferences, even if message copy is not sent to self.
///
/// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by
/// Autocrypt Level 1, version 1.1) and for messages sent in protected groups.
/// Autocrypt Level 1, version 1.1) and for messages sent in verified groups.
///
/// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
///
/// Always returns `false` if one of the peerstates does not support Autocrypt (is in "reset"
/// state) or does not have a known key.
pub fn should_encrypt(
&self,
context: &Context,
@@ -81,11 +84,7 @@ impl EncryptHelper {
match peerstate.prefer_encrypt {
EncryptPreference::NoPreference => {}
EncryptPreference::Mutual => prefer_encrypt_count += 1,
EncryptPreference::Reset => {
if !e2ee_guaranteed {
return Ok(false);
}
}
EncryptPreference::Reset => return Ok(false),
};
}
None => {
@@ -235,9 +234,9 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail
}
}
async fn decrypt_if_autocrypt_message(
async fn decrypt_if_autocrypt_message<'a>(
context: &Context,
mail: &ParsedMail<'_>,
mail: &ParsedMail<'a>,
private_keyring: Keyring<SignedSecretKey>,
public_keyring_for_validate: Keyring<SignedPublicKey>,
ret_valid_signatures: &mut HashSet<Fingerprint>,
@@ -511,59 +510,4 @@ Sent with my Delta Chat Messenger: https://delta.chat";
Ok(())
}
fn new_peerstates(
ctx: &Context,
prefer_encrypt: EncryptPreference,
) -> Vec<(Option<Peerstate<'_>>, &str)> {
let addr = "bob@foo.bar";
let pub_key = bob_keypair().public;
let peerstate = Peerstate {
context: &ctx,
addr: addr.into(),
last_seen: 13,
last_seen_autocrypt: 14,
prefer_encrypt,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key: Some(pub_key.clone()),
gossip_timestamp: 15,
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
let mut peerstates = Vec::new();
peerstates.push((Some(peerstate), addr));
peerstates
}
#[async_std::test]
async fn test_should_encrypt() {
let t = TestContext::new_alice().await;
let encrypt_helper = EncryptHelper::new(&t.ctx).await.unwrap();
// test with EncryptPreference::NoPreference:
// if e2ee_eguaranteed is unset, there is no encryption as not more than half of peers want encryption
let ps = new_peerstates(&t.ctx, EncryptPreference::NoPreference);
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
// test with EncryptPreference::Reset
let ps = new_peerstates(&t.ctx, EncryptPreference::Reset);
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
// test with EncryptPreference::Mutual (self is also Mutual)
let ps = new_peerstates(&t.ctx, EncryptPreference::Mutual);
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
// test with missing peerstate
let mut ps = Vec::new();
ps.push((None, "bob@foo.bar"));
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
}
}

View File

@@ -457,6 +457,24 @@ impl Imap {
Ok(())
}
async fn get_config_last_seen_uid<S: AsRef<str>>(
&self,
context: &Context,
folder: S,
) -> (u32, u32) {
let key = format!("imap.mailbox.{}", folder.as_ref());
if let Some(entry) = context.sql.get_raw_config(context, &key).await {
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
let mut parts = entry.split(':');
(
parts.next().unwrap_or_default().parse().unwrap_or(0),
parts.next().unwrap_or_default().parse().unwrap_or(0),
)
} else {
(0, 0)
}
}
/// Synchronizes UIDs in the database with UIDs on the server.
///
/// It is assumed that no operations are taking place on the same
@@ -541,7 +559,7 @@ impl Imap {
self.select_folder(context, Some(folder)).await?;
// compare last seen UIDVALIDITY against the current one
let (uid_validity, last_seen_uid) = get_config_last_seen_uid(context, &folder).await;
let (uid_validity, last_seen_uid) = self.get_config_last_seen_uid(context, &folder).await;
let config = &mut self.config;
let mailbox = config
@@ -567,7 +585,8 @@ impl Imap {
// id we do not do this here, we'll miss the first message
// as we will get in here again and fetch from lastseenuid+1 then
set_config_last_seen_uid(context, &folder, new_uid_validity, 0).await;
self.set_config_last_seen_uid(context, &folder, new_uid_validity, 0)
.await;
return Ok((new_uid_validity, 0));
}
@@ -611,7 +630,8 @@ impl Imap {
}
};
set_config_last_seen_uid(context, &folder, new_uid_validity, new_last_seen_uid).await;
self.set_config_last_seen_uid(context, &folder, new_uid_validity, new_last_seen_uid)
.await;
if uid_validity != 0 || last_seen_uid != 0 {
job::schedule_resync(context).await;
}
@@ -694,7 +714,8 @@ impl Imap {
let last_one = new_last_seen_uid.max(new_last_seen_uid_processed);
if last_one > last_seen_uid {
set_config_last_seen_uid(context, &folder, uid_validity, last_one).await;
self.set_config_last_seen_uid(context, &folder, uid_validity, last_one)
.await;
}
if read_errors == 0 {
@@ -840,6 +861,23 @@ impl Imap {
Ok(msgs)
}
async fn set_config_last_seen_uid<S: AsRef<str>>(
&self,
context: &Context,
folder: S,
uidvalidity: u32,
lastseenuid: u32,
) {
let key = format!("imap.mailbox.{}", folder.as_ref());
let val = format!("{}:{}", uidvalidity, lastseenuid);
context
.sql
.set_raw_config(context, &key, Some(&val))
.await
.ok();
}
/// Fetches a list of messages by server UID.
/// The passed in list of uids must be sorted.
///
@@ -1673,36 +1711,6 @@ fn get_fallback_folder(delimiter: &str) -> String {
format!("INBOX{}DeltaChat", delimiter)
}
pub async fn set_config_last_seen_uid<S: AsRef<str>>(
context: &Context,
folder: S,
uidvalidity: u32,
lastseenuid: u32,
) {
let key = format!("imap.mailbox.{}", folder.as_ref());
let val = format!("{}:{}", uidvalidity, lastseenuid);
context
.sql
.set_raw_config(context, &key, Some(&val))
.await
.ok();
}
async fn get_config_last_seen_uid<S: AsRef<str>>(context: &Context, folder: S) -> (u32, u32) {
let key = format!("imap.mailbox.{}", folder.as_ref());
if let Some(entry) = context.sql.get_raw_config(context, &key).await {
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
let mut parts = entry.split(':');
(
parts.next().unwrap_or_default().parse().unwrap_or(0),
parts.next().unwrap_or_default().parse().unwrap_or(0),
)
} else {
(0, 0)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -31,7 +31,6 @@ use crate::param::*;
use crate::pgp;
use crate::sql::{self, Sql};
use crate::stock::StockMessage;
use ::pgp::types::KeyTrait;
use async_tar::Archive;
// Name of the database file in the backup.
@@ -79,7 +78,11 @@ pub enum ImexMode {
///
/// Only one import-/export-progress can run at the same time.
/// To cancel an import-/export-progress, drop the future returned by this function.
pub async fn imex(context: &Context, what: ImexMode, param1: impl AsRef<Path>) -> Result<()> {
pub async fn imex(
context: &Context,
what: ImexMode,
param1: Option<impl AsRef<Path>>,
) -> Result<()> {
let cancel = context.alloc_ongoing().await?;
let res = async {
@@ -410,8 +413,6 @@ async fn set_self_key(
},
)
.await?;
info!(context, "stored self key: {:?}", keypair.secret.key_id());
Ok(())
}
@@ -438,11 +439,19 @@ pub fn normalize_setup_code(s: &str) -> String {
out
}
async fn imex_inner(context: &Context, what: ImexMode, path: impl AsRef<Path>) -> Result<()> {
info!(context, "Import/export dir: {}", path.as_ref().display());
ensure!(context.sql.is_open().await, "Database not opened.");
async fn imex_inner(
context: &Context,
what: ImexMode,
param: Option<impl AsRef<Path>>,
) -> Result<()> {
ensure!(param.is_some(), "No Import/export dir/file given.");
info!(context, "Import/export process started.");
context.emit_event(EventType::ImexProgress(10));
ensure!(context.sql.is_open().await, "Database not opened.");
let path = param.ok_or_else(|| format_err!("Imex: Param was None"))?;
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
// before we export anything, make sure the private key exists
if e2ee::ensure_secret_key_exists(context).await.is_err() {
@@ -866,12 +875,6 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
continue;
}
}
info!(
context,
"considering key file: {}",
path_plus_name.display()
);
match dc_read_file(context, &path_plus_name).await {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
@@ -961,7 +964,7 @@ where
let any_key = key as &dyn Any;
let kind = if any_key.downcast_ref::<SignedPublicKey>().is_some() {
"public"
} else if any_key.downcast_ref::<SignedSecretKey>().is_some() {
} else if any_key.downcast_ref::<SignedPublicKey>().is_some() {
"private"
} else {
"unknown"
@@ -969,12 +972,7 @@ where
let id = id.map_or("default".into(), |i| i.to_string());
dir.as_ref().join(format!("{}-key-{}.asc", kind, &id))
};
info!(
context,
"Exporting key {:?} to {}",
key.key_id(),
file_name.display()
);
info!(context, "Exporting key {}", file_name.display());
dc_delete_file(context, &file_name).await;
let content = key.to_asc(None).into_bytes();
@@ -1042,7 +1040,7 @@ mod tests {
}
#[async_std::test]
async fn test_export_public_key_to_asc_file() {
async fn test_export_key_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().public;
let blobdir = "$BLOBDIR";
@@ -1056,37 +1054,6 @@ mod tests {
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[async_std::test]
async fn test_export_private_key_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().secret;
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
.await
.is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{}/private-key-default.asc", blobdir);
let bytes = async_std::fs::read(&filename).await.unwrap();
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[async_std::test]
async fn test_export_and_import_key() {
let context = TestContext::new().await;
context.configure_alice().await;
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir).await {
panic!("got error on export: {:?}", err);
}
let context2 = TestContext::new().await;
context2.configure_alice().await;
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir).await {
panic!("got error on import: {:?}", err);
}
}
#[test]
fn test_normalize_setup_code() {
let norm = normalize_setup_code("123422343234423452346234723482349234");

View File

@@ -627,9 +627,6 @@ impl Job {
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
/// and show them in the chat list.
async fn fetch_existing_msgs(&mut self, context: &Context, imap: &mut Imap) -> Status {
if context.get_config_bool(Config::Bot).await {
return Status::Finished(Ok(())); // Bots don't want those messages
}
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
@@ -639,19 +636,17 @@ impl Job {
add_all_recipients_as_contacts(context, imap, Config::ConfiguredMvboxFolder).await;
add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).await;
if context.get_config_bool(Config::FetchExistingMsgs).await {
for config in &[
Config::ConfiguredMvboxFolder,
Config::ConfiguredInboxFolder,
Config::ConfiguredSentboxFolder,
] {
if let Some(folder) = context.get_config(*config).await {
if let Err(e) = imap.fetch_new_messages(context, folder, true).await {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(context, "Could not fetch messages, retrying: {:#}", e);
return Status::RetryLater;
};
}
for config in &[
Config::ConfiguredMvboxFolder,
Config::ConfiguredInboxFolder,
Config::ConfiguredSentboxFolder,
] {
if let Some(folder) = context.get_config(*config).await {
if let Err(e) = imap.fetch_new_messages(context, folder, true).await {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(context, "Could not fetch messages, retrying: {:#}", e);
return Status::RetryLater;
};
}
}
info!(context, "Done fetching existing messages.");

View File

@@ -431,9 +431,11 @@ mod tests {
use std::error::Error;
use async_std::sync::Arc;
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
lazy_static! {
static ref KEYPAIR: KeyPair = alice_keypair();
}
#[test]
fn test_from_armored_string() {

View File

@@ -544,7 +544,9 @@ impl Message {
return ret;
};
let contact = if self.from_id != DC_CONTACT_ID_SELF as u32 && chat.typ == Chattype::Group {
let contact = if self.from_id != DC_CONTACT_ID_SELF as u32
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
Contact::get_by_id(context, self.from_id).await.ok()
} else {
None
@@ -589,10 +591,6 @@ impl Message {
|| cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
}
pub fn get_info_type(&self) -> SystemMessage {
self.param.get_cmd()
}
pub fn is_system_message(&self) -> bool {
let cmd = self.param.get_cmd();
cmd != SystemMessage::Unknown
@@ -978,7 +976,7 @@ impl Lot {
);
self.text1_meaning = Meaning::Text1Self;
}
} else if chat.typ == Chattype::Group {
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
if msg.is_info() || contact.is_none() {
self.text1 = None;
self.text1_meaning = Meaning::None;
@@ -998,23 +996,16 @@ impl Lot {
}
}
let mut text2 = get_summarytext_by_raw(
msg.viewtype,
msg.text.as_ref(),
&msg.param,
SUMMARY_CHARACTERS,
context,
)
.await;
if text2.is_empty() && msg.quoted_text().is_some() {
text2 = context
.stock_str(StockMessage::ReplyNoun)
.await
.into_owned()
}
self.text2 = Some(text2);
self.text2 = Some(
get_summarytext_by_raw(
msg.viewtype,
msg.text.as_ref(),
&msg.param,
SUMMARY_CHARACTERS,
context,
)
.await,
);
self.timestamp = msg.get_timestamp();
self.state = msg.state.into();
@@ -1196,7 +1187,6 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
"jpg" => (Viewtype::Image, "image/jpeg"),
"json" => (Viewtype::File, "application/json"),
"mov" => (Viewtype::Video, "video/quicktime"),
"m4a" => (Viewtype::Audio, "audio/m4a"),
"mp3" => (Viewtype::Audio, "audio/mpeg"),
"mp4" => (Viewtype::Video, "video/mp4"),
"odp" => (
@@ -1638,16 +1628,14 @@ pub(crate) async fn handle_ndn(
context: &Context,
failed: &FailureReport,
error: Option<impl AsRef<str>>,
) -> anyhow::Result<()> {
) {
if failed.rfc724_mid.is_empty() {
return Ok(());
return;
}
// The NDN might be for a message-id that had attachments and was sent from a non-Delta Chat client.
// In this case we need to mark multiple "msgids" as failed that all refer to the same message-id.
let msgs: Vec<_> = context
let res = context
.sql
.query_map(
.query_row(
concat!(
"SELECT",
" m.id AS msg_id,",
@@ -1664,42 +1652,37 @@ pub(crate) async fn handle_ndn(
row.get::<_, Chattype>("type")?,
))
},
|rows| Ok(rows.collect::<Vec<_>>()),
)
.await?;
for (i, msg) in msgs.into_iter().enumerate() {
let (msg_id, chat_id, chat_type) = msg?;
set_msg_failed(context, msg_id, error.as_ref()).await;
if i == 0 {
// Add only one info msg for all failed messages
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
}
.await;
if let Err(ref err) = res {
info!(context, "Failed to select NDN {:?}", err);
}
Ok(())
}
if let Ok((msg_id, chat_id, chat_type)) = res {
set_msg_failed(context, msg_id, error).await;
async fn ndn_maybe_add_info_msg(
context: &Context,
failed: &FailureReport,
chat_id: ChatId,
chat_type: Chattype,
) -> anyhow::Result<()> {
if chat_type == Chattype::Group {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown).await;
let contact = Contact::load_from_db(context, contact_id).await?;
// Tell the user which of the recipients failed if we know that (because in a group, this might otherwise be unclear)
let text = context
.stock_string_repl_str(StockMessage::FailedSendingTo, contact.get_display_name())
.await;
chat::add_info_msg(context, chat_id, text).await;
context.emit_event(EventType::ChatModified(chat_id));
if chat_type == Chattype::Group || chat_type == Chattype::VerifiedGroup {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown).await;
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
// Tell the user which of the recipients failed if we know that (because in a group, this might otherwise be unclear)
chat::add_info_msg(
context,
chat_id,
context
.stock_string_repl_str(
StockMessage::FailedSendingTo,
contact.get_display_name(),
)
.await,
)
.await;
context.emit_event(EventType::ChatModified(chat_id));
}
}
}
}
Ok(())
}
/// The number of messages assigned to real chat (!=deaddrop, !=trash)

View File

@@ -1,9 +1,5 @@
use chrono::TimeZone;
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
use std::collections::HashSet;
use vcard::properties::Photo;
use vcard::values::image_value::ImageValue;
use vcard::{Set, VCard};
use crate::blob::BlobObject;
use crate::chat::{self, Chat};
@@ -237,7 +233,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
fn is_e2ee_guaranteed(&self) -> bool {
match &self.loaded {
Loaded::Message { chat } => {
if chat.is_protected() {
if chat.typ == Chattype::VerifiedGroup {
return true;
}
@@ -259,7 +255,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
fn min_verified(&self) -> PeerstateVerifiedStatus {
match &self.loaded {
Loaded::Message { chat } => {
if chat.is_protected() {
if chat.typ == Chattype::VerifiedGroup {
PeerstateVerifiedStatus::BidirectVerified
} else {
PeerstateVerifiedStatus::Unverified
@@ -272,7 +268,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
fn should_force_plaintext(&self) -> bool {
match &self.loaded {
Loaded::Message { chat } => {
if chat.is_protected() {
if chat.typ == Chattype::VerifiedGroup {
false
} else {
self.msg
@@ -349,7 +345,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.stock_str(StockMessage::AcSetupMsgSubject)
.await
.into_owned()
} else if chat.typ == Chattype::Group {
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
let re = if self.in_reply_to.is_empty() {
""
} else {
@@ -712,11 +708,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let mut placeholdertext = None;
let mut meta_part = None;
if chat.is_protected() {
if chat.typ == Chattype::VerifiedGroup {
protected_headers.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
}
if chat.typ == Chattype::Group {
if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
protected_headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
let encoded = encode_words(&chat.name);
@@ -850,18 +846,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
};
}
}
SystemMessage::ChatProtectionEnabled => {
protected_headers.push(Header::new(
"Chat-Content".to_string(),
"protection-enabled".to_string(),
));
}
SystemMessage::ChatProtectionDisabled => {
protected_headers.push(Header::new(
"Chat-Content".to_string(),
"protection-disabled".to_string(),
));
}
_ => {}
}
@@ -995,24 +979,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
if self.attach_selfavatar {
match context.get_config(Config::Selfavatar).await {
Some(path) => {
match build_selfavatar_file(context, &path) {
Ok((part, filename)) => {
parts.push(part);
protected_headers.push(Header::new("Chat-User-Avatar".into(), filename))
}
Err(err) => {
warn!(context, "mimefactory: cannot attach selfavatar: {}", err)
}
};
match build_vcard_part(context, &path).await {
Ok(part) => {
parts.push(part);
}
Err(err) => warn!(context, "mimefactory: cannot build vCard: {}", err),
};
}
Some(path) => match build_selfavatar_file(context, &path) {
Ok((part, filename)) => {
parts.push(part);
protected_headers.push(Header::new("Chat-User-Avatar".into(), filename))
}
Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err),
},
None => protected_headers.push(Header::new("Chat-User-Avatar".into(), "0".into())),
}
}
@@ -1156,23 +1129,13 @@ async fn build_body_file(
Viewtype::Image | Viewtype::Gif => format!(
"{}.{}",
if base_name.is_empty() {
chrono::Utc
.timestamp(msg.timestamp_sort as i64, 0)
.format("image_%Y-%m-%d_%H-%M-%S")
.to_string()
"image"
} else {
base_name.to_string()
base_name
},
&suffix,
),
Viewtype::Video => format!(
"video_{}.{}",
chrono::Utc
.timestamp(msg.timestamp_sort as i64, 0)
.format("%Y-%m-%d_%H-%M-%S")
.to_string(),
&suffix
),
Viewtype::Video => format!("video.{}", &suffix),
_ => blob.as_file_name().to_string(),
};
@@ -1214,40 +1177,6 @@ async fn build_body_file(
Ok((mail, filename_to_send))
}
async fn build_vcard_file(context: &Context, avatar_path: &str) -> Result<String, Error> {
let avatar_blob = BlobObject::from_path(context, avatar_path)?;
let displayname = context
.get_config(Config::Displayname)
.await
.unwrap_or_default();
let mut vcard = VCard::from_formatted_name_str(&displayname)?;
// TODO: add KIND:individual
let mut photos = HashSet::new();
if let Ok(image_value) = ImageValue::from_file(avatar_blob.to_abs_path()) {
let photo = Photo::from_image_value(image_value);
photos.insert(photo);
}
vcard.photos = Set::from_hash_set(photos).ok();
Ok(vcard.to_string())
}
async fn build_vcard_part(context: &Context, avatar_path: &str) -> Result<PartBuilder, Error> {
let body = build_vcard_file(context, avatar_path).await?;
let encoded_body = wrapped_base64_encode(&body.as_bytes());
let part = PartBuilder::new()
.content_type(&mime::TEXT_VCARD)
.header((
"Content-Disposition",
"attachment; filename=\"{avatar.vcf}\"",
))
.header(("Content-Transfer-Encoding", "base64"))
.body(encoded_body);
Ok(part)
}
fn build_selfavatar_file(context: &Context, path: &str) -> Result<(PartBuilder, String), Error> {
let blob = BlobObject::from_path(context, path)?;
let filename_to_send = match blob.suffix() {

View File

@@ -3,9 +3,9 @@ use std::future::Future;
use std::pin::Pin;
use deltachat_derive::{FromSql, ToSql};
use lazy_static::lazy_static;
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use once_cell::sync::Lazy;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
@@ -86,10 +86,6 @@ pub enum SystemMessage {
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged = 10,
// Chat protection state changed
ChatProtectionEnabled = 11,
ChatProtectionDisabled = 12,
}
impl Default for SystemMessage {
@@ -127,7 +123,6 @@ impl MimeMessage {
// remove headers that are allowed _only_ in the encrypted part
headers.remove("secure-join-fingerprint");
headers.remove("chat-verified");
// Memory location for a possible decrypted message.
let mail_raw;
@@ -223,7 +218,6 @@ impl MimeMessage {
failure_report: None,
};
parser.parse_mime_recursive(context, &mail).await?;
parser.maybe_remove_bad_parts().await;
parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context)?;
@@ -259,10 +253,6 @@ impl MimeMessage {
self.is_system_message = SystemMessage::LocationStreamingEnabled;
} else if value == "ephemeral-timer-changed" {
self.is_system_message = SystemMessage::EphemeralTimerChanged;
} else if value == "protection-enabled" {
self.is_system_message = SystemMessage::ChatProtectionEnabled;
} else if value == "protection-disabled" {
self.is_system_message = SystemMessage::ChatProtectionDisabled;
}
}
Ok(())
@@ -295,8 +285,7 @@ impl MimeMessage {
/// Squashes mutlipart chat messages with attachment into single-part messages.
///
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
/// containing a description. If such a message is detected, text from the first part can be
/// moved to the second part, and the first part dropped.
/// containing an explanation. If such a message is detected, first part can be safely dropped.
#[allow(clippy::indexing_slicing)]
fn squash_attachment_parts(&mut self) {
if let [textpart, filepart] = &self.parts[..] {
@@ -316,9 +305,6 @@ impl MimeMessage {
// insert new one
filepart.msg = self.parts[0].msg.clone();
if let Some(quote) = self.parts[0].param.get(Param::Quote) {
filepart.param.set(Param::Quote, quote);
}
// forget the one we use now
self.parts[0].msg = "".to_string();
@@ -718,17 +704,12 @@ impl MimeMessage {
}
};
let mut dehtml_failed = false;
let (simplified_txt, is_forwarded, top_quote) = if decoded_data.is_empty() {
("".to_string(), false, None)
} else {
let is_html = mime_type == mime::TEXT_HTML;
let out = if is_html {
dehtml(&decoded_data).unwrap_or_else(|| {
dehtml_failed = true;
decoded_data.clone()
})
dehtml(&decoded_data)
} else {
decoded_data.clone()
};
@@ -758,9 +739,8 @@ impl MimeMessage {
(simplified_txt, top_quote)
};
if !simplified_txt.is_empty() || simplified_quote.is_some() {
if !simplified_txt.is_empty() {
let mut part = Part::default();
part.dehtlm_failed = dehtml_failed;
part.typ = Viewtype::Text;
part.mimetype = Some(mime_type);
part.msg = simplified_txt;
@@ -814,26 +794,6 @@ impl MimeMessage {
return;
}
}
if mime_type.type_() == "text/x-vcard"
|| mime_type.type_() == "text/vcard"
|| filename.ends_with(".vcf")
|| filename.ends_with(".vcard")
{
println!("Parsing vcard {:?}", String::from_utf8_lossy(decoded_data));
for contact in ical::VcardParser::new(decoded_data) {
println!("Parsing contact {:?}", contact);
if let Ok(contact) = contact {
for property in contact.properties {
println!("Parsed property {:?}", property);
if property.name == "email" {
}
}
}
}
return;
}
/* we have a regular file attachment,
write decoded data to new blob object */
@@ -888,7 +848,6 @@ impl MimeMessage {
}
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
self.is_system_message = SystemMessage::Unknown;
if let Some(part) = self.parts.first_mut() {
part.typ = Viewtype::Text;
part.msg = format!("[{}]", error_msg.as_ref());
@@ -1023,21 +982,11 @@ impl MimeMessage {
Ok(None)
}
async fn maybe_remove_bad_parts(&mut self) {
let good_parts = self.parts.iter().filter(|p| !p.dehtlm_failed).count();
if good_parts == 0 {
// We have no good part but show at least one bad part in order to show anything at all
self.parts.truncate(1);
} else if good_parts < self.parts.len() {
self.parts.retain(|p| !p.dehtlm_failed);
}
}
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
/// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
/// Also you should add a test in dc_receive_imf.rs (there already are lots of test_parse_ndn_* tests).
#[allow(clippy::indexing_slicing)]
async fn heuristically_parse_ndn(&mut self, context: &Context) {
async fn heuristically_parse_ndn(&mut self, context: &Context) -> Option<()> {
let maybe_ndn = if let Some(from) = self.get(HeaderDef::From_) {
let from = from.to_ascii_lowercase();
from.contains("mailer-daemon") || from.contains("mail-daemon")
@@ -1045,8 +994,9 @@ impl MimeMessage {
false
};
if maybe_ndn && self.failure_report.is_none() {
static RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"Message-ID:(.*)").unwrap());
lazy_static! {
static ref RE: regex::Regex = regex::Regex::new(r"Message-ID:(.*)").unwrap();
}
for captures in self
.parts
.iter()
@@ -1066,6 +1016,7 @@ impl MimeMessage {
}
}
}
None // Always return None, we just return anything so that we can use the '?' operator.
}
/// Handle reports
@@ -1095,9 +1046,7 @@ impl MimeMessage {
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
if let Err(e) = message::handle_ndn(context, failure_report, error).await {
warn!(context, "Could not handle ndn: {}", e);
}
message::handle_ndn(context, failure_report, error).await
}
}
@@ -1236,7 +1185,6 @@ pub struct Part {
pub param: Params,
org_filename: Option<String>,
pub error: Option<String>,
dehtlm_failed: bool,
}
/// return mimetype and viewtype for a parsed mail
@@ -1652,22 +1600,6 @@ mod tests {
assert_eq!(mimeparser.group_avatar, None);
}
#[async_std::test]
async fn test_mimeparser_with_vcard() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/vcard.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert_eq!(
mimeparser.parts[0].msg,
"vCard example An example of vCard sent from Thunderbird."
);
assert_eq!(mimeparser.user_avatar, None);
assert_eq!(mimeparser.group_avatar, None);
}
#[async_std::test]
async fn test_mimeparser_message_kml() {
let context = TestContext::new().await;
@@ -1934,55 +1866,6 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
assert_eq!(message.parts[0].msg, "Hello!");
}
#[async_std::test]
async fn test_hide_html_without_content() {
let t = TestContext::new().await;
let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC)
From: sender@example.com
To: receiver@example.com
Subject: Mail with inline attachment
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_Part_25_46172632.1581201680436"
------=_Part_25_46172632.1581201680436
Content-Type: text/html; charset=utf-8
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Windows-1252">
<meta name="GENERATOR" content="MSHTML 11.00.10570.1001"></head>
<body><img align="baseline" alt="" src="cid:1712254131-1" border="0" hspace="0">
</body>
------=_Part_25_46172632.1581201680436
Content-Type: application/pdf; name="some_pdf.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="some_pdf.pdf"
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
Y29kZT4+CnN0cmVhbQp4nGVOuwoCMRDs8xVbC8aZvC4Hx4Hno7ATAhZi56MTtPH33YtXiLKQ3ZnM
MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
------=_Part_25_46172632.1581201680436--
"#;
let message = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::File);
assert_eq!(message.parts[0].msg, "");
// Make sure the file is there even though the html is wrong:
let param = &message.parts[0].param;
let blob: BlobObject = param
.get_blob(Param::File, &t.ctx, false)
.await
.unwrap()
.unwrap();
let f = async_std::fs::File::open(blob.to_abs_path()).await.unwrap();
let size = f.metadata().await.unwrap().len();
assert_eq!(size, 154);
}
#[async_std::test]
async fn parse_inline_image() {
let context = TestContext::new().await;
@@ -2276,85 +2159,4 @@ Reply
);
assert_eq!(message.parts[0].msg, "Reply");
}
#[async_std::test]
async fn parse_quote_without_reply() {
let context = TestContext::new().await;
let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Subject: Re: swipe-to-reply
MIME-Version: 1.0
In-Reply-To: <bar@example.org>
Date: Tue, 06 Oct 2020 00:00:00 +0000
Message-ID: <foo@example.org>
To: bob <bob@example.org>
From: alice <alice@example.org>
> Just a quote.
"##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(
message.get_subject(),
Some("Re: swipe-to-reply".to_string())
);
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::Text);
assert_eq!(
message.parts[0].param.get(Param::Quote).unwrap(),
"Just a quote."
);
assert_eq!(message.parts[0].msg, "");
}
#[async_std::test]
async fn parse_quote_top_posting() {
let context = TestContext::new().await;
let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Subject: Re: top posting
MIME-Version: 1.0
In-Reply-To: <bar@example.org>
Message-ID: <foo@example.org>
To: bob <bob@example.org>
From: alice <alice@example.org>
A reply.
On 2020-10-25, Bob wrote:
> A quote.
"##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(message.get_subject(), Some("Re: top posting".to_string()));
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::Text);
assert_eq!(
message.parts[0].param.get(Param::Quote).unwrap(),
"A quote."
);
assert_eq!(message.parts[0].msg, "A reply.");
}
#[async_std::test]
async fn test_attachment_quote() {
let context = TestContext::new().await;
let raw = include_bytes!("../test-data/message/quote_attach.eml");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(mimeparser.get_subject().unwrap(), "Message from Alice");
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].msg, "Reply");
assert_eq!(
mimeparser.parts[0].param.get(Param::Quote).unwrap(),
"Quote"
);
assert_eq!(mimeparser.parts[0].typ, Viewtype::File);
}
}

View File

@@ -381,7 +381,7 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
mod tests {
use super::*;
use crate::test_utils::*;
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
#[test]
fn test_split_armored_data_1() {
@@ -449,29 +449,26 @@ mod tests {
/// The original text of [CTEXT_SIGNED]
static CLEARTEXT: &[u8] = b"This is a test";
/// Initialised [TestKeys] for tests.
static KEYS: Lazy<TestKeys> = Lazy::new(TestKeys::new);
lazy_static! {
/// Initialised [TestKeys] for tests.
static ref KEYS: TestKeys = TestKeys::new();
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
static CTEXT_SIGNED: Lazy<String> = Lazy::new(|| {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(
CLEARTEXT,
keyring,
Some(KEYS.alice_secret.clone()),
))
.unwrap()
});
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
static ref CTEXT_SIGNED: String = {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))).unwrap()
};
/// A cyphertext encrypted to Alice & Bob, not signed.
static CTEXT_UNSIGNED: Lazy<String> = Lazy::new(|| {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
});
/// A cyphertext encrypted to Alice & Bob, not signed.
static ref CTEXT_UNSIGNED: String = {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
};
}
#[test]
fn test_encrypt_signed() {

File diff suppressed because it is too large Load Diff

View File

@@ -75,7 +75,6 @@ pub struct Provider {
pub server: Vec<Server>,
pub config_defaults: Option<Vec<ConfigDefault>>,
pub strict_tls: bool,
pub max_smtp_rcpt_to: Option<u16>,
pub oauth2_authorizer: Option<Oauth2Authorizer>,
}

View File

@@ -42,8 +42,8 @@ def process_config_defaults(data):
config_defaults = data.get("config_defaults", "")
for key in config_defaults:
value = str(config_defaults[key])
defaults += " ConfigDefault { key: Config::" + camel(key) + ", value: \"" + value + "\" },\n"
defaults += " ])"
defaults += " ConfigDefault { key: Config::" + camel(key) + ", value: \"" + value + "\" },\n"
defaults += " ])"
return defaults
@@ -66,7 +66,7 @@ def process_data(data, file):
raise TypeError("domain used twice: " + domain)
domains_dict[domain] = True
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
comment += domain + ", "
@@ -96,7 +96,7 @@ def process_data(data, file):
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
raise TypeError("bad username pattern")
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern + " },\n")
config_defaults = process_config_defaults(data)
@@ -104,9 +104,6 @@ def process_data(data, file):
strict_tls = data.get("strict_tls", False)
strict_tls = "true" if strict_tls else "false"
max_smtp_rcpt_to = data.get("max_smtp_rcpt_to", 0)
max_smtp_rcpt_to = "Some(" + str(max_smtp_rcpt_to) + ")" if max_smtp_rcpt_to != 0 else "None"
oauth2 = data.get("oauth2", "")
oauth2 = "Some(Oauth2Authorizer::" + camel(oauth2) + ")" if oauth2 != "" else "None"
@@ -114,17 +111,16 @@ def process_data(data, file):
before_login_hint = cleanstr(data.get("before_login_hint", ""))
after_login_hint = cleanstr(data.get("after_login_hint", ""))
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
provider += "static " + file2varname(file) + ": Lazy<Provider> = Lazy::new(|| Provider {\n"
provider += " status: Status::" + status + ",\n"
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
provider += " overview_page: \"" + file2url(file) + "\",\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " strict_tls: " + strict_tls + ",\n"
provider += " max_smtp_rcpt_to: " + max_smtp_rcpt_to + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
provider += "});\n\n"
provider += " static ref " + file2varname(file) + ": Provider = Provider {\n"
provider += " status: Status::" + status + ",\n"
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
provider += " overview_page: \"" + file2url(file) + "\",\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " strict_tls: " + strict_tls + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
provider += " };\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")
@@ -133,7 +129,7 @@ def process_data(data, file):
# finally, add the provider
global out_all, out_domains
out_all += "// " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
out_all += " // " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
# also add provider with no special things to do -
# eg. _not_ supporting oauth2 is also an information and we can skip the mx-lookup in this case
@@ -168,16 +164,18 @@ if __name__ == "__main__":
"use crate::provider::UsernamePattern::*;\n"
"use crate::provider::*;\n"
"use std::collections::HashMap;\n\n"
"use once_cell::sync::Lazy;\n\n")
"lazy_static::lazy_static! {\n\n")
process_dir(sys.argv[1])
out_all += "pub static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| [\n"
out_all += " pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [\n"
out_all += out_domains;
out_all += "].iter().copied().collect());\n\n"
out_all += " ].iter().copied().collect();\n\n"
now = datetime.datetime.utcnow()
out_all += "pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "\
"Lazy::new(|| chrono::NaiveDate::from_ymd("+str(now.year)+", "+str(now.month)+", "+str(now.day)+"));\n"
out_all += " pub static ref PROVIDER_UPDATED: chrono::NaiveDate = "\
"chrono::NaiveDate::from_ymd("+str(now.year)+", "+str(now.month)+", "+str(now.day)+");\n"
out_all += "}"
print(out_all)

View File

@@ -1,6 +1,6 @@
//! # QR code module
use once_cell::sync::Lazy;
use lazy_static::lazy_static;
use percent_encoding::percent_decode_str;
use serde::Deserialize;
@@ -358,10 +358,12 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Lot {
Lot::from_address(context, name, addr).await
}
static VCARD_NAME_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
static VCARD_EMAIL_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
lazy_static! {
static ref VCARD_NAME_RE: regex::Regex =
regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap();
static ref VCARD_EMAIL_RE: regex::Regex =
regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap();
}
/// Extract address for the matmsg scheme.
///

View File

@@ -69,11 +69,9 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
}
Some(job) => {
// Let the fetch run, but return back to the job afterwards.
info!(ctx, "postponing imap-job {} to run fetch...", job);
jobs_loaded = 0;
if ctx.get_config_bool(Config::InboxWatch).await {
info!(ctx, "postponing imap-job {} to run fetch...", job);
fetch(&ctx, &mut connection).await;
}
fetch(&ctx, &mut connection).await;
}
None => {
jobs_loaded = 0;

View File

@@ -348,7 +348,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
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_protected, _blocked)) => break chatid,
Ok((chatid, _is_verified, _blocked)) => break chatid,
Err(err) => {
if start.elapsed() > Duration::from_secs(7) {
return Err(JoinError::MissingChat(err));
@@ -791,19 +791,19 @@ pub(crate) async fn handle_securejoin_handshake(
let vg_expect_encrypted = if join_vg {
let group_id = get_qr_attr!(context, text2).to_string();
// This is buggy, is_protected_group will always be
// This is buggy, is_verified_group will always be
// false since the group is created by receive_imf by
// the very handshake message we're handling now. But
// only after we have returned. It does not impact
// the security invariants of secure-join however.
let (_, is_protected_group, _) = chat::get_chat_id_by_grpid(context, &group_id)
let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, &group_id)
.await
.unwrap_or((ChatId::new(0), false, Blocked::Not));
// when joining a non-verified group
// the vg-member-added message may be unencrypted
// when not all group members have keys or prefer encryption.
// So only expect encryption if this is a verified group
is_protected_group
is_verified_group
} else {
// setup contact is always encrypted
true
@@ -1102,7 +1102,6 @@ mod tests {
use super::*;
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::peerstate::Peerstate;
use crate::test_utils::TestContext;
@@ -1329,7 +1328,7 @@ mod tests {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chatid = chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat")
let chatid = chat::create_group_chat(&alice.ctx, VerifiedStatus::Verified, "the chat")
.await
.unwrap();
@@ -1428,6 +1427,6 @@ mod tests {
let bob_chatid = joiner.await;
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await.unwrap();
assert!(bob_chat.is_protected());
assert!(bob_chat.is_verified());
}
}

View File

@@ -71,24 +71,25 @@ pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool, Opti
let lines = split_lines(&input);
let (lines, is_forwarded) = skip_forward_header(&lines);
let (lines, mut top_quote) = remove_top_quote(lines);
let original_lines = &lines;
let lines = remove_message_footer(lines);
let (lines, top_quote) = remove_top_quote(lines);
let text = if is_chat_message {
render_message(lines, false)
render_message(lines, false, false)
} else {
let (lines, has_nonstandard_footer) = remove_nonstandard_footer(lines);
let (lines, mut bottom_quote) = remove_bottom_quote(lines);
if top_quote.is_none() && bottom_quote.is_some() {
std::mem::swap(&mut top_quote, &mut bottom_quote);
}
let (lines, has_bottom_quote) = remove_bottom_quote(lines);
if lines.iter().all(|it| it.trim().is_empty()) {
render_message(original_lines, false)
render_message(original_lines, false, false)
} else {
render_message(lines, has_nonstandard_footer || bottom_quote.is_some())
render_message(
lines,
top_quote.is_some(),
has_nonstandard_footer || has_bottom_quote,
)
}
};
(text, is_forwarded, top_quote)
@@ -109,27 +110,16 @@ fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
}
#[allow(clippy::indexing_slicing)]
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
let mut first_quoted_line = lines.len();
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
let mut last_quoted_line = None;
for (l, line) in lines.iter().enumerate().rev() {
if is_plain_quote(line) {
if last_quoted_line.is_none() {
first_quoted_line = l + 1;
}
last_quoted_line = Some(l)
} else if !is_empty_line(line) {
break;
}
}
if let Some(mut l_last) = last_quoted_line {
let quoted_text = lines[l_last..first_quoted_line]
.iter()
.map(|s| {
s.strip_prefix(">")
.map_or(*s, |u| u.strip_prefix(" ").unwrap_or(u))
})
.join("\n");
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
l_last -= 1
}
@@ -139,9 +129,9 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
l_last -= 1
}
}
(&lines[..l_last], Some(quoted_text))
(&lines[..l_last], true)
} else {
(lines, None)
(lines, false)
}
}
@@ -183,8 +173,11 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
}
}
fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
fn render_message(lines: &[&str], is_cut_at_begin: bool, is_cut_at_end: bool) -> String {
let mut ret = String::new();
if is_cut_at_begin {
ret += "[...]";
}
/* we write empty lines only in case and non-empty line follows */
let mut pending_linebreaks = 0;
let mut empty_body = true;
@@ -207,7 +200,7 @@ fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
pending_linebreaks = 1
}
}
if is_cut_at_end && !empty_body {
if is_cut_at_end && (!is_cut_at_begin || !empty_body) {
ret += " [...]";
}
// redo escaping done by escape_message_footer_marks()

View File

@@ -192,8 +192,7 @@ impl Smtp {
};
let security = match lp.security {
Socket::Plain => smtp::ClientSecurity::None,
Socket::STARTTLS => smtp::ClientSecurity::Required(tls_parameters),
Socket::STARTTLS | Socket::Plain => smtp::ClientSecurity::Opportunistic(tls_parameters),
_ => smtp::ClientSecurity::Wrapper(tls_parameters),
};

View File

@@ -3,11 +3,8 @@
use super::Smtp;
use async_smtp::*;
use crate::config::Config;
use crate::constants::DEFAULT_MAX_SMTP_RCPT_TO;
use crate::context::Context;
use crate::events::EventType;
use crate::provider::get_provider_info;
use itertools::Itertools;
use std::time::Duration;
@@ -37,51 +34,37 @@ impl Smtp {
) -> Result<()> {
let message_len_bytes = message.len();
let mut chunk_size = DEFAULT_MAX_SMTP_RCPT_TO;
if let Some(provider) = get_provider_info(
&context
.get_config(Config::ConfiguredAddr)
let recipients_display = recipients.iter().map(|x| x.to_string()).join(",");
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
message,
);
if let Some(ref mut transport) = self.transport {
// The timeout is 1min + 3min per MB.
let timeout = 60 + (180 * message_len_bytes / 1_000_000) as u64;
transport
.send_with_timeout(mail, Some(&Duration::from_secs(timeout)))
.await
.unwrap_or_default(),
) {
if let Some(max_smtp_rcpt_to) = provider.max_smtp_rcpt_to {
chunk_size = max_smtp_rcpt_to as usize;
}
}
.map_err(Error::SendError)?;
for recipients_chunk in recipients.chunks(chunk_size).into_iter() {
let recipients = recipients_chunk.to_vec();
let recipients_display = recipients.iter().map(|x| x.to_string()).join(",");
context.emit_event(EventType::SmtpMessageSent(format!(
"Message len={} was smtp-sent to {}",
message_len_bytes, recipients_display
)));
self.last_success = Some(std::time::SystemTime::now());
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
&message,
Ok(())
} else {
warn!(
context,
"uh? SMTP has no transport, failed to send to {}", recipients_display
);
if let Some(ref mut transport) = self.transport {
// The timeout is 1min + 3min per MB.
let timeout = 60 + (180 * message_len_bytes / 1_000_000) as u64;
transport
.send_with_timeout(mail, Some(&Duration::from_secs(timeout)))
.await
.map_err(Error::SendError)?;
context.emit_event(EventType::SmtpMessageSent(format!(
"Message len={} was smtp-sent to {}",
message_len_bytes, recipients_display
)));
self.last_success = Some(std::time::SystemTime::now());
} else {
warn!(
context,
"uh? SMTP has no transport, failed to send to {}", recipients_display
);
return Err(Error::NoTransport);
}
Err(Error::NoTransport)
}
Ok(())
}
}

View File

@@ -1360,7 +1360,7 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
}
if dbversion < 68 {
info!(context, "[migration] v68");
// the index is used to speed up get_fresh_msg_cnt() (see comment there for more details) and marknoticed_chat()
// the index is used to speed up get_fresh_msg_cnt(), see comment there for more details
sql.execute(
"CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);",
paramsv![],
@@ -1368,20 +1368,6 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
.await?;
sql.set_raw_config_int(context, "dbversion", 68).await?;
}
if dbversion < 69 {
info!(context, "[migration] v69");
sql.execute(
"ALTER TABLE chats ADD COLUMN protected INTEGER DEFAULT 0;",
paramsv![],
)
.await?;
sql.execute(
"UPDATE chats SET protected=1, type=120 WHERE type=130;", // 120=group, 130=old verified group
paramsv![],
)
.await?;
sql.set_raw_config_int(context, "dbversion", 69).await?;
}
// (2) updates that require high-level objects
// (the structure is complete now and all objects are usable)

View File

@@ -5,8 +5,8 @@ use std::borrow::Cow;
use strum::EnumProperty;
use strum_macros::EnumProperty;
use crate::blob::BlobObject;
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::*;
use crate::context::Context;
@@ -14,7 +14,6 @@ use crate::error::{bail, Error};
use crate::message::Message;
use crate::param::Param;
use crate::stock::StockMessage::{DeviceMessagesHint, WelcomeMessage};
use crate::{blob::BlobObject, config::Config};
/// Stock strings
///
@@ -234,20 +233,6 @@ pub enum StockMessage {
fallback = "Could not find your mail server.\n\nPlease check your internet connection."
))]
ErrorNoNetwork = 87,
#[strum(props(fallback = "Chat protection enabled."))]
ProtectionEnabled = 88,
#[strum(props(fallback = "Chat protection disabled."))]
ProtectionDisabled = 89,
// used in summaries, a noun, not a verb (not: "to reply")
#[strum(props(fallback = "Reply"))]
ReplyNoun = 90,
#[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\
To use the \"Saved messages\" feature again, create a new chat with yourself."))]
SelfDeletedMsgBody = 91,
}
/*
@@ -413,27 +398,16 @@ impl Context {
}
}
/// Returns a stock message saying that protection status has changed.
pub async fn stock_protection_msg(&self, protect: ProtectionStatus, from_id: u32) -> String {
self.stock_system_msg(
match protect {
ProtectionStatus::Protected => StockMessage::ProtectionEnabled,
ProtectionStatus::Unprotected => StockMessage::ProtectionDisabled,
},
"",
"",
from_id,
)
.await
}
pub(crate) async fn update_device_chats(&self) -> Result<(), Error> {
if self.get_config_bool(Config::Bot).await {
pub async fn update_device_chats(&self) -> Result<(), Error> {
// check for the LAST added device message - if it is present, we can skip message creation.
// this is worthwhile as this function is typically called
// by the UI on every program start or even on every opening of the chatlist.
if chat::was_device_msg_ever_added(&self, "core-welcome").await? {
return Ok(());
}
// create saved-messages chat; we do this only once, if the user has deleted the chat,
// he can recreate it manually (make sure we do not re-add it when configure() was called a second time)
// create saved-messages chat;
// we do this only once, if the user has deleted the chat, he can recreate it manually.
if !self.sql.get_raw_config_bool(&self, "self-chat-added").await {
self.sql
.set_raw_config_bool(&self, "self-chat-added", true)
@@ -467,7 +441,6 @@ mod tests {
use crate::constants::DC_CONTACT_ID_SELF;
use crate::chat::Chat;
use crate::chatlist::Chatlist;
use num_traits::ToPrimitive;
@@ -659,31 +632,8 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2);
let chat0 = Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
.await
.unwrap();
let (self_talk_id, device_chat_id) = if chat0.is_self_talk() {
(chats.get_chat_id(0), chats.get_chat_id(1))
} else {
(chats.get_chat_id(1), chats.get_chat_id(0))
};
// delete self-talk first; this adds a message to device-chat about how self-talk can be restored
let device_chat_msgs_before = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None)
.await
.len();
self_talk_id.delete(&t.ctx).await.ok();
assert_eq!(
chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None)
.await
.len(),
device_chat_msgs_before + 1
);
// delete device chat
device_chat_id.delete(&t.ctx).await.ok();
// check, that the chatlist is empty
chats.get_chat_id(0).delete(&t.ctx).await.ok();
chats.get_chat_id(1).delete(&t.ctx).await.ok();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);

View File

@@ -9,15 +9,13 @@ use async_std::path::PathBuf;
use async_std::sync::RwLock;
use tempfile::{tempdir, TempDir};
use crate::chat;
use crate::chat::{ChatId, ChatItem};
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::message::Message;
use crate::mimeparser::MimeMessage;
use crate::param::{Param, Params};
@@ -189,19 +187,6 @@ impl TestContext {
.await
.unwrap();
}
/// Get the most recent message of a chat.
///
/// Panics on errors or if the most recent message is a marker.
pub async fn get_last_msg(&self, chat_id: ChatId) -> Message {
let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None).await;
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
msg_id
} else {
panic!("Wrong item type");
};
Message::load_from_db(&self.ctx, *msg_id).await.unwrap()
}
}
/// A raw message as it was scheduled to be sent.

View File

@@ -1,27 +0,0 @@
Subject: Message from Alice
MIME-Version: 1.0
In-Reply-To: <Mr.7h_HyOuM3Dz.xcp4v8QiQua@testrun.org>
Date: Sun, 08 Nov 2020 01:16:26 +0000
Chat-Version: 1.0
Message-ID: <Mr.126R7OsBKvk.dGGBC5WcsLF@testrun.org>
To: Bob <bob@example.org>
From: Alice <alice@testrun.org>
Content-Type: multipart/mixed; boundary="uWbWY2IyEtJ8wZmp282Na11hxBBXlV"
--uWbWY2IyEtJ8wZmp282Na11hxBBXlV
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
> Quote
Reply
--uWbWY2IyEtJ8wZmp282Na11hxBBXlV
Content-Type: text/plain
Content-Disposition: attachment; filename="attachment.txt"
Content-Transfer-Encoding: base64
ZGF0YSB0byBzZW5k
--uWbWY2IyEtJ8wZmp282Na11hxBBXlV--

View File

@@ -1,38 +0,0 @@
Return-Path: <alice@example.org>
Delivered-To: bob@example.org
To: bob@example.org
From: Alice <alice@example.org>
Subject: vCard example
Message-ID: <foobar@example.org>
Date: Sun, 22 Nov 2020 22:44:00 +0000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101
Thunderbird/78.4.3
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="------------01973B3475205164D5F9F507"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------01973B3475205164D5F9F507
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
An example of vCard sent from Thunderbird.
--------------01973B3475205164D5F9F507
Content-Type: text/x-vcard; charset=utf-8;
name="alice.vcf"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
filename="alice.vcf"
begin:vcard
fn:Alice Foobar
n:Foobar;Alice
email;internet:alice@example.org
x-mozilla-html:FALSE
version:2.1
end:vcard
--------------01973B3475205164D5F9F507--

110
tests/stress.rs Normal file
View File

@@ -0,0 +1,110 @@
//! Stress some functions for testing; if used as a lib, this file is obsolete.
use deltachat::config;
use deltachat::context::*;
use tempfile::tempdir;
/* some data used for testing
******************************************************************************/
async fn stress_functions(context: &Context) {
let res = context
.get_config(config::Config::SysConfigKeys)
.await
.unwrap();
assert!(!res.contains(" probably_never_a_key "));
assert!(res.contains(" addr "));
assert!(res.contains(" mail_server "));
assert!(res.contains(" mail_user "));
assert!(res.contains(" mail_pw "));
assert!(res.contains(" mail_port "));
assert!(res.contains(" send_server "));
assert!(res.contains(" send_user "));
assert!(res.contains(" send_pw "));
assert!(res.contains(" send_port "));
assert!(res.contains(" server_flags "));
assert!(res.contains(" imap_folder "));
assert!(res.contains(" displayname "));
assert!(res.contains(" selfstatus "));
assert!(res.contains(" selfavatar "));
assert!(res.contains(" e2ee_enabled "));
assert!(res.contains(" mdns_enabled "));
assert!(res.contains(" save_mime_headers "));
assert!(res.contains(" configured_addr "));
assert!(res.contains(" configured_mail_server "));
assert!(res.contains(" configured_mail_user "));
assert!(res.contains(" configured_mail_pw "));
assert!(res.contains(" configured_mail_port "));
assert!(res.contains(" configured_send_server "));
assert!(res.contains(" configured_send_user "));
assert!(res.contains(" configured_send_pw "));
assert!(res.contains(" configured_send_port "));
assert!(res.contains(" configured_server_flags "));
// Cant check, no configured context
// assert!(dc_is_configured(context) != 0, "Missing configured context");
// let setupcode = dc_create_setup_code(context);
// let setupcode_c = CString::new(setupcode.clone()).unwrap();
// let setupfile = dc_render_setup_file(context, &setupcode).unwrap();
// let setupfile_c = CString::new(setupfile).unwrap();
// let mut headerline_2: *const libc::c_char = ptr::null();
// let payload = dc_decrypt_setup_file(context, setupcode_c.as_ptr(), setupfile_c.as_ptr());
// assert!(payload.is_null());
// assert!(!dc_split_armored_data(
// payload,
// &mut headerline_2,
// ptr::null_mut(),
// ptr::null_mut(),
// ptr::null_mut(),
// ));
// assert!(!headerline_2.is_null());
// assert_eq!(
// strcmp(
// headerline_2,
// b"-----BEGIN PGP PRIVATE KEY BLOCK-----\x00" as *const u8 as *const libc::c_char,
// ),
// 0
// );
// free(payload as *mut libc::c_void);
// Cant check, no configured context
// assert!(dc_is_configured(context) != 0, "missing configured context");
// let qr = dc_get_securejoin_qr(context, 0);
// assert!(!qr.is_null(), "Invalid qr code generated");
// let qr_r = as_str(qr);
// assert!(qr_r.len() > 55);
// assert!(qr_r.starts_with("OPENPGP4FPR:"));
// let res = dc_check_qr(context, qr);
// let s = res.get_state();
// assert!(
// s == QrState::AskVerifyContact
// || s == QrState::FprMissmatch
// || s == QrState::FprWithoutAddr
// );
// free(qr.cast());
}
async fn create_test_context() -> Context {
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)
.await
.unwrap()
}
#[async_std::test]
async fn test_stress_tests() {
let context = create_test_context().await;
stress_functions(&context).await;
}