mirror of
https://github.com/chatmail/core.git
synced 2026-04-12 11:12:11 +03:00
Compare commits
68 Commits
smooth-pro
...
test_imex_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff34ed6054 | ||
|
|
8aaea09011 | ||
|
|
c005f756d6 | ||
|
|
3c6d52842e | ||
|
|
4d2542cee5 | ||
|
|
9bf8799484 | ||
|
|
e15372531e | ||
|
|
0ae9443e22 | ||
|
|
67cddedf7e | ||
|
|
bf72ae4ccc | ||
|
|
c7863c67bf | ||
|
|
3d108fedc4 | ||
|
|
546e8dedce | ||
|
|
64cd48a4e1 | ||
|
|
2e118b773e | ||
|
|
6206c82ee5 | ||
|
|
e7736138a8 | ||
|
|
78b44cb4d0 | ||
|
|
0a9a2394d8 | ||
|
|
6e37c1442e | ||
|
|
c323798386 | ||
|
|
b8e98c0b81 | ||
|
|
7a82fd4bbd | ||
|
|
0a300da347 | ||
|
|
aa26c52813 | ||
|
|
19697e255e | ||
|
|
07e4762f71 | ||
|
|
6ec743f8b1 | ||
|
|
010be693e1 | ||
|
|
d8a7a178c2 | ||
|
|
18e9073bfe | ||
|
|
0032468a87 | ||
|
|
7e793a518c | ||
|
|
e5b0194e8c | ||
|
|
13055b9c87 | ||
|
|
5661e0b8f1 | ||
|
|
1672905c71 | ||
|
|
d13d62105a | ||
|
|
0b80b81129 | ||
|
|
9b72aba8e3 | ||
|
|
1f24c5f8a4 | ||
|
|
7f882a6406 | ||
|
|
50f3af58f8 | ||
|
|
8425e23d82 | ||
|
|
9fc6bbf41f | ||
|
|
1e2e042244 | ||
|
|
03d86360d6 | ||
|
|
4eb8d3fef6 | ||
|
|
da727740ab | ||
|
|
3a993a4b77 | ||
|
|
45dae1ff0c | ||
|
|
f144426bf5 | ||
|
|
e447bdc0c3 | ||
|
|
c1768bb311 | ||
|
|
66cb3d4358 | ||
|
|
47f4f2bd08 | ||
|
|
12cf89735c | ||
|
|
d240bbcd07 | ||
|
|
5e07a36cd2 | ||
|
|
49b5962568 | ||
|
|
a7998c190c | ||
|
|
b8a55f3aa4 | ||
|
|
ab8bf3c2f3 | ||
|
|
d05dd977d9 | ||
|
|
8b3494b5c1 | ||
|
|
d9a45eb931 | ||
|
|
c2b222e6a5 | ||
|
|
4c8e6ef495 |
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
- breaking change: `dc_update_device_chats()` was removed. This is now done automatically during configure.
|
||||
|
||||
- Added a `bot` config. Currently, it only prevents filling the device chats automatically.
|
||||
|
||||
## 1.46.0
|
||||
|
||||
- breaking change: `dc_configure()` report errors in
|
||||
|
||||
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -224,13 +224,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caadebe62f5ad78d6418a47a86b5b84439a52bdd5c893e55a5c81974da111455"
|
||||
checksum = "0bdc2bb91844456110d27da8f712f4cbc693f3538521a43cb79c6b2e6f717f3e"
|
||||
dependencies = [
|
||||
"async-native-tls",
|
||||
"async-std",
|
||||
"base64 0.12.3",
|
||||
"base64 0.13.0",
|
||||
"byte-pool",
|
||||
"chrono",
|
||||
"futures",
|
||||
@@ -446,6 +446,12 @@ 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 = "bit-set"
|
||||
version = "0.5.2"
|
||||
@@ -1027,7 +1033,6 @@ dependencies = [
|
||||
"indexmap",
|
||||
"itertools",
|
||||
"kamadak-exif",
|
||||
"lazy_static",
|
||||
"lettre_email",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -1035,6 +1040,7 @@ dependencies = [
|
||||
"native-tls",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pgp",
|
||||
"pretty_assertions",
|
||||
@@ -1783,9 +1789,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "imap-proto"
|
||||
version = "0.10.2"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16a6def1d5ac8975d70b3fd101d57953fe3278ef2ee5d7816cba54b1d1dfc22f"
|
||||
checksum = "3091b99ee5b80f9b010eb6f962af9495ad06561bf662126b077e8ca30e463182"
|
||||
dependencies = [
|
||||
"nom 5.1.2",
|
||||
]
|
||||
@@ -2926,9 +2932,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sanitize-filename"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23fd0fec94ec480abfd86bb8f4f6c57e0efb36dac5c852add176ea7b04c74801"
|
||||
checksum = "bf18934a12018228c5b55a6dae9df5d0641e3566b3630cb46cc55564068e7c2f"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
|
||||
@@ -34,7 +34,7 @@ serde_json = "1.0"
|
||||
chrono = "0.4.6"
|
||||
indexmap = "1.3.0"
|
||||
kamadak-exif = "0.5"
|
||||
lazy_static = "1.4.0"
|
||||
once_cell = "1.4.1"
|
||||
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.2.1"
|
||||
sanitize-filename = "0.3.0"
|
||||
stop-token = { version = "0.1.1", features = ["unstable"] }
|
||||
mailparse = "0.13.0"
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
|
||||
@@ -349,6 +349,10 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
|
||||
* The type `jitsi:` may be handled by external apps.
|
||||
* If no type is prefixed, the videochat is handled completely in a browser.
|
||||
* - `bot` = Set to "1" if this is a bot. E.g. prevents adding the "Device messages" and "Saved messages" chats.
|
||||
* - `fetch_existing` = 1=fetch most recent existing messages on configure (default),
|
||||
* 0=do not fetch existing messages on configure.
|
||||
* In both cases, existing recipients are added to the contact database.
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -984,24 +988,6 @@ void dc_set_draft (dc_context_t* context, uint32_t ch
|
||||
*/
|
||||
uint32_t dc_add_device_msg (dc_context_t* context, const char* label, dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Init device-messages and saved-messages chat.
|
||||
* This function adds the device-chat and saved-messages chat
|
||||
* and adds one or more welcome or update-messages.
|
||||
* The ui can add messages on its own using dc_add_device_msg() -
|
||||
* for ordering, either before or after or even without calling this function.
|
||||
*
|
||||
* Chat and message creation is done only once.
|
||||
* So if the user has manually deleted things, they won't be re-created
|
||||
* (however, not seen device messages are added and may re-create the device-chat).
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
*/
|
||||
void dc_update_device_chats (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a device-message with a given label was ever added.
|
||||
* Device-messages can be added dc_add_device_msg().
|
||||
@@ -1170,6 +1156,24 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
|
||||
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
|
||||
|
||||
|
||||
/**
|
||||
* Enable or disable protection against active attacks.
|
||||
* To enable protection, it is needed that all members are verified;
|
||||
* if this condition is met, end-to-end-encryption is always enabled
|
||||
* and only the verified keys are used.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED on changes
|
||||
* and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The ID of the chat to change the protection for.
|
||||
* @param protect 1=protect chat, 0=unprotect chat
|
||||
* @return 1=success, 0=error, eg. some members may be unverified
|
||||
*/
|
||||
int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect);
|
||||
|
||||
|
||||
/**
|
||||
* Set chat visibility to pinned, archived or normal.
|
||||
*
|
||||
@@ -1299,15 +1303,15 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param verified If set to 1 the function creates a secure verified group.
|
||||
* Only secure-verified members are allowed in these groups
|
||||
* @param protect If set to 1 the function creates group with protection initially enabled.
|
||||
* Only verified members are allowed in these groups
|
||||
* and end-to-end-encryption is always enabled.
|
||||
* @param name The name of the group chat to create.
|
||||
* The name may be changed later using dc_set_chat_name().
|
||||
* To find out the name of a group later, see dc_chat_get_name()
|
||||
* @return The chat ID of the new group chat, 0 on errors.
|
||||
*/
|
||||
uint32_t dc_create_group_chat (dc_context_t* context, int verified, const char* name);
|
||||
uint32_t dc_create_group_chat (dc_context_t* context, int protect, const char* name);
|
||||
|
||||
|
||||
/**
|
||||
@@ -1329,7 +1333,7 @@ int dc_is_contact_in_chat (dc_context_t* context, uint32_t ch
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* If the group is a verified group, only verified contacts can be added to the group.
|
||||
* If the group has group protection enabled, only verified contacts can be added to the group.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*
|
||||
@@ -1973,7 +1977,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
* @param context The context object.
|
||||
* @param chat_id If set to a group-chat-id,
|
||||
* the Verified-Group-Invite protocol is offered in the QR code;
|
||||
* works for verified groups as well as for normal groups.
|
||||
* works for protected groups as well as for normal groups.
|
||||
* If set to 0, the Setup-Contact protocol is offered in the QR code.
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
@@ -2003,7 +2007,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
|
||||
* When the protocol has finished, an info-message is added to that chat.
|
||||
* - If the given QR code starts the Verified-Group-Invite protocol,
|
||||
* the function waits until the protocol has finished.
|
||||
* This is because the verified group is not opportunistic
|
||||
* This is because the protected group is not opportunistic
|
||||
* and can be created only when the contacts have verified each other.
|
||||
*
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
@@ -2015,8 +2019,8 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
|
||||
* to dc_check_qr().
|
||||
* @return Chat-id of the joined chat, the UI may redirect to the this chat.
|
||||
* If the out-of-band verification failed or was aborted, 0 is returned.
|
||||
* A returned chat-id does not guarantee that the chat or the belonging contact is verified.
|
||||
* If needed, this be checked with dc_chat_is_verified() and dc_contact_is_verified(),
|
||||
* A returned chat-id does not guarantee that the chat is protected or the belonging contact is verified.
|
||||
* If needed, this be checked with dc_chat_is_protected() and dc_contact_is_verified(),
|
||||
* however, in practise, the UI will just listen to #DC_EVENT_CONTACTS_CHANGED unconditionally.
|
||||
*/
|
||||
uint32_t dc_join_securejoin (dc_context_t* context, const char* qr);
|
||||
@@ -2769,7 +2773,6 @@ char* dc_chat_get_info_json (dc_context_t* context, size_t chat
|
||||
#define DC_CHAT_TYPE_UNDEFINED 0
|
||||
#define DC_CHAT_TYPE_SINGLE 100
|
||||
#define DC_CHAT_TYPE_GROUP 120
|
||||
#define DC_CHAT_TYPE_VERIFIED_GROUP 130
|
||||
|
||||
|
||||
/**
|
||||
@@ -2810,9 +2813,6 @@ uint32_t dc_chat_get_id (const dc_chat_t* chat);
|
||||
* - DC_CHAT_TYPE_GROUP (120) - a group chat, chats_contacts contain all group
|
||||
* members, incl. DC_CONTACT_ID_SELF
|
||||
*
|
||||
* - DC_CHAT_TYPE_VERIFIED_GROUP (130) - a verified group chat. In verified groups,
|
||||
* all members are verified and encryption is always active and cannot be disabled.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return Chat type.
|
||||
@@ -2942,15 +2942,16 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a chat is verified. Verified chats contain only verified members
|
||||
* and encryption is alwasy enabled. Verified chats are created using
|
||||
* dc_create_group_chat() by setting the 'verified' parameter to true.
|
||||
* Check if a chat is protected.
|
||||
* Protected chats contain only verified members and encryption is always enabled.
|
||||
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
|
||||
* The status can be changed using dc_set_chat_protection().
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat verified, 0=chat is not verified
|
||||
* @return 1=chat protected, 0=chat is not protected
|
||||
*/
|
||||
int dc_chat_is_verified (const dc_chat_t* chat);
|
||||
int dc_chat_is_protected (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
@@ -3444,7 +3445,8 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
|
||||
/**
|
||||
* Check if the message is an informational message, created by the
|
||||
* device or by another users. Such messages are not "typed" by the user but
|
||||
* created due to other actions, eg. dc_set_chat_name(), dc_set_chat_profile_image()
|
||||
* created due to other actions,
|
||||
* eg. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
|
||||
* or dc_add_contact_to_chat().
|
||||
*
|
||||
* These messages are typically shown in the center of the chat view,
|
||||
@@ -3460,6 +3462,32 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
|
||||
int dc_msg_is_info (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get the type of an informational message.
|
||||
* If dc_msg_is_info() returns 1, this function returns the type of the informational message.
|
||||
* UIs can display eg. an icon based upon the type.
|
||||
*
|
||||
* Currently, the following types are defined:
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
|
||||
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
||||
*
|
||||
* Even when you display an icon,
|
||||
* you should still display the text of the informational message using dc_msg_get_text()
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return One of the DC_INFO* constants.
|
||||
* 0 or other values indicate unspecified types
|
||||
* or that the message is not an info-message.
|
||||
*/
|
||||
int dc_msg_get_info_type (const dc_msg_t* msg);
|
||||
|
||||
|
||||
// DC_INFO* uses the same values as SystemMessage in rust-land
|
||||
#define DC_INFO_PROTECTION_ENABLED 11
|
||||
#define DC_INFO_PROTECTION_DISABLED 12
|
||||
|
||||
|
||||
/**
|
||||
* Check if a message is still in creation. A message is in creation between
|
||||
* the calls to dc_prepare_msg() and dc_send_msg().
|
||||
@@ -4615,7 +4643,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
* Messages were marked noticed or seen.
|
||||
* The ui may update badge counters or stop showing a chatlist-item with a bold font.
|
||||
*
|
||||
* This event is emitted eg. when calling dc_markseen_msgs(), dc_marknoticed_chat() or dc_marknoticed_contact().
|
||||
* This event is emitted eg. when calling dc_markseen_msgs(), dc_marknoticed_chat() or dc_marknoticed_contact()
|
||||
* or when a chat is answered on another device.
|
||||
* Do not try to derive the state of an item from just the fact you received the event;
|
||||
* use eg. dc_msg_get_state() or dc_get_fresh_msg_cnt() for this purpose.
|
||||
*
|
||||
@@ -4956,8 +4985,9 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_STR_BAD_TIME_MSG_BODY 85
|
||||
#define DC_STR_UPDATE_REMINDER_MSG_BODY 86
|
||||
#define DC_STR_ERROR_NO_NETWORK 87
|
||||
|
||||
#define DC_STR_COUNT 87
|
||||
#define DC_STR_PROTECTION_ENABLED 88
|
||||
#define DC_STR_PROTECTION_DISABLED 89
|
||||
#define DC_STR_REPLY_NOUN 90 /* eg. "Reply", used in summaries, a noun, not a verb (not: "to reply") */
|
||||
|
||||
/*
|
||||
* @}
|
||||
|
||||
@@ -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};
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, Origin};
|
||||
use deltachat::context::Context;
|
||||
@@ -807,21 +807,6 @@ 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,
|
||||
@@ -1057,6 +1042,32 @@ 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,
|
||||
@@ -1170,7 +1181,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,
|
||||
verified: libc::c_int,
|
||||
protect: libc::c_int,
|
||||
name: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if context.is_null() || name.is_null() {
|
||||
@@ -1178,14 +1189,15 @@ pub unsafe extern "C" fn dc_create_group_chat(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let verified = if let Some(s) = contact::VerifiedStatus::from_i32(verified) {
|
||||
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
|
||||
s
|
||||
} else {
|
||||
warn!(ctx, "bad protect-value for dc_create_group_chat()");
|
||||
return 0;
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
chat::create_group_chat(&ctx, verified, to_string_lossy(name))
|
||||
chat::create_group_chat(&ctx, protect, to_string_lossy(name))
|
||||
.await
|
||||
.log_err(ctx, "Failed to create group chat")
|
||||
.map(|id| id.to_u32())
|
||||
@@ -2389,13 +2401,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_verified(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_is_verified()");
|
||||
eprintln!("ignoring careless call to dc_chat_is_protected()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.is_verified() as libc::c_int
|
||||
ffi_chat.chat.is_protected() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2817,6 +2829,16 @@ 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() {
|
||||
|
||||
@@ -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};
|
||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, ProtectionStatus};
|
||||
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\
|
||||
createverified <name>\n\
|
||||
createprotected <name>\n\
|
||||
addmember <contact-id>\n\
|
||||
removemember <contact-id>\n\
|
||||
groupname <name>\n\
|
||||
@@ -379,6 +379,8 @@ 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\
|
||||
@@ -523,7 +525,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(),
|
||||
@@ -533,6 +535,7 @@ 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 {
|
||||
@@ -607,7 +610,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(),
|
||||
@@ -624,6 +627,11 @@ 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? {
|
||||
@@ -654,15 +662,16 @@ 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, VerifiedStatus::Unverified, arg1).await?;
|
||||
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
|
||||
|
||||
println!("Group#{} created successfully.", chat_id);
|
||||
}
|
||||
"createverified" => {
|
||||
"createprotected" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?;
|
||||
let chat_id =
|
||||
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
|
||||
|
||||
println!("VerifiedGroup#{} created successfully.", chat_id);
|
||||
println!("Group#{} created and protected successfully.", chat_id);
|
||||
}
|
||||
"addmember" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected");
|
||||
@@ -872,9 +881,6 @@ 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.");
|
||||
|
||||
@@ -906,7 +912,21 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"archive" => ChatVisibility::Archived,
|
||||
"unarchive" | "unpin" => ChatVisibility::Normal,
|
||||
"pin" => ChatVisibility::Pinned,
|
||||
_ => panic!("Unexpected command (This should never happen)"),
|
||||
_ => 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),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -77,6 +77,7 @@ 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()
|
||||
|
||||
|
||||
@@ -336,6 +336,9 @@ 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.
|
||||
|
||||
@@ -57,10 +57,7 @@ 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) in (
|
||||
const.DC_CHAT_TYPE_GROUP,
|
||||
const.DC_CHAT_TYPE_VERIFIED_GROUP
|
||||
)
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
||||
|
||||
def is_deaddrop(self):
|
||||
""" return true if this chat is a deaddrop chat.
|
||||
@@ -85,12 +82,20 @@ class Chat(object):
|
||||
"""
|
||||
return not lib.dc_chat_is_unpromoted(self._dc_chat)
|
||||
|
||||
def is_verified(self):
|
||||
""" return True if this chat is a verified group.
|
||||
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
|
||||
|
||||
:returns: True if chat is verified, False otherwise.
|
||||
:returns: True if the chat is writable, False otherwise
|
||||
"""
|
||||
return lib.dc_chat_is_verified(self._dc_chat)
|
||||
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)
|
||||
|
||||
def get_name(self):
|
||||
""" return name of this chat.
|
||||
|
||||
@@ -354,6 +354,8 @@ 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():
|
||||
|
||||
@@ -20,6 +20,14 @@ class ImexTracker:
|
||||
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
||||
self._imex_events.put(ffi_event.data2)
|
||||
|
||||
def wait_progress(self, target_progress, progress_timeout=60):
|
||||
while True:
|
||||
ev = self._imex_events.get(timeout=progress_timeout)
|
||||
if isinstance(ev, int) and ev >= target_progress:
|
||||
return ev
|
||||
if ev == 1000 or ev == 0:
|
||||
return None
|
||||
|
||||
def wait_finish(self, progress_timeout=60):
|
||||
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||
files_written = []
|
||||
|
||||
@@ -6,6 +6,7 @@ import queue
|
||||
import time
|
||||
from deltachat import const, Account
|
||||
from deltachat.message import Message
|
||||
from deltachat.tracker import ImexTracker
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@@ -1032,6 +1033,16 @@ 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)
|
||||
@@ -1235,8 +1246,16 @@ class TestOnlineAccount:
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
|
||||
lp.sec("export all to {}".format(backupdir))
|
||||
path = ac1.export_all(backupdir.strpath)
|
||||
assert os.path.exists(path)
|
||||
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
path = ac1.export_all(backupdir.strpath)
|
||||
assert os.path.exists(path)
|
||||
|
||||
# check progress events for export
|
||||
imex_tracker.wait_progress(250)
|
||||
imex_tracker.wait_progress(500)
|
||||
imex_tracker.wait_progress(750)
|
||||
imex_tracker.wait_progress(1000)
|
||||
# return mex_tracker.wait_finish()
|
||||
t = time.time()
|
||||
|
||||
lp.sec("get fresh empty account")
|
||||
@@ -1247,7 +1266,15 @@ class TestOnlineAccount:
|
||||
assert path2 == path
|
||||
|
||||
lp.sec("import backup and check it's proper")
|
||||
ac2.import_all(path)
|
||||
with ac2.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
ac2.import_all(path)
|
||||
|
||||
# check progress events for import
|
||||
imex_tracker.wait_progress(250)
|
||||
imex_tracker.wait_progress(500)
|
||||
imex_tracker.wait_progress(750)
|
||||
imex_tracker.wait_progress(1000)
|
||||
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
@@ -1343,7 +1370,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_verified()
|
||||
assert chat1.is_protected()
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
@@ -1362,7 +1389,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_verified()
|
||||
assert msg.chat.is_protected()
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: send message and let ac1 read it")
|
||||
@@ -2084,6 +2111,7 @@ 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")
|
||||
|
||||
@@ -2091,6 +2119,7 @@ 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")
|
||||
|
||||
@@ -2098,5 +2127,6 @@ 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")
|
||||
|
||||
2
spec.md
2
spec.md
@@ -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://www.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
|
||||
by the [Protected Headers](https://tools.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
|
||||
|
||||
|
||||
# Outgoing messages
|
||||
|
||||
392
src/chat.rs
392
src/chat.rs
@@ -1,5 +1,6 @@
|
||||
//! # Chat module
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use std::convert::TryFrom;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -45,6 +46,33 @@ 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
|
||||
@@ -146,6 +174,109 @@ 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,
|
||||
@@ -329,6 +460,12 @@ 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(
|
||||
@@ -538,6 +675,7 @@ pub struct Chat {
|
||||
pub param: Params,
|
||||
is_sending_locations: bool,
|
||||
pub mute_duration: MuteDuration,
|
||||
protected: ProtectionStatus,
|
||||
}
|
||||
|
||||
impl Chat {
|
||||
@@ -547,7 +685,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.blocked, c.locations_send_until, c.muted_until, c.protected
|
||||
FROM chats c
|
||||
WHERE c.id=?;",
|
||||
paramsv![chat_id],
|
||||
@@ -562,6 +700,7 @@ 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)
|
||||
},
|
||||
@@ -727,9 +866,9 @@ impl Chat {
|
||||
!self.is_unpromoted()
|
||||
}
|
||||
|
||||
/// Returns true if chat is a verified group chat.
|
||||
pub fn is_verified(&self) -> bool {
|
||||
self.typ == Chattype::VerifiedGroup
|
||||
/// Returns true if chat protection is enabled.
|
||||
pub fn is_protected(&self) -> bool {
|
||||
self.protected == ProtectionStatus::Protected
|
||||
}
|
||||
|
||||
/// Returns true if location streaming is enabled in the chat.
|
||||
@@ -756,15 +895,12 @@ impl Chat {
|
||||
let mut to_id = 0;
|
||||
let mut location_id = 0;
|
||||
|
||||
if !(self.typ == Chattype::Single
|
||||
|| self.typ == Chattype::Group
|
||||
|| self.typ == Chattype::VerifiedGroup)
|
||||
{
|
||||
if !(self.typ == Chattype::Single || self.typ == Chattype::Group) {
|
||||
error!(context, "Cannot send to chat type #{}.", self.typ,);
|
||||
bail!("Cannot set to chat type #{}", self.typ);
|
||||
}
|
||||
|
||||
if (self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup)
|
||||
if self.typ == Chattype::Group
|
||||
&& !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await
|
||||
{
|
||||
emit_event!(
|
||||
@@ -781,7 +917,7 @@ impl Chat {
|
||||
|
||||
let new_rfc724_mid = {
|
||||
let grpid = match self.typ {
|
||||
Chattype::Group | Chattype::VerifiedGroup => Some(self.grpid.as_str()),
|
||||
Chattype::Group => Some(self.grpid.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
dc_create_outgoing_rfc724_mid(grpid, &from)
|
||||
@@ -805,7 +941,7 @@ impl Chat {
|
||||
);
|
||||
bail!("Cannot set message, contact for {} not found.", self.id);
|
||||
}
|
||||
} else if (self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup)
|
||||
} else if self.typ == Chattype::Group
|
||||
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
|
||||
{
|
||||
msg.param.set_int(Param::AttachGroupImage, 1);
|
||||
@@ -1000,7 +1136,7 @@ pub struct ChatInfo {
|
||||
///
|
||||
/// On the C API this number is one of the
|
||||
/// `DC_CHAT_TYPE_UNDEFINED`, `DC_CHAT_TYPE_SINGLE`,
|
||||
/// `DC_CHAT_TYPE_GROUP` or `DC_CHAT_TYPE_VERIFIED_GROUP`
|
||||
/// or `DC_CHAT_TYPE_GROUP`
|
||||
/// constants.
|
||||
#[serde(rename = "type")]
|
||||
pub type_: u32,
|
||||
@@ -1687,12 +1823,34 @@ 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 chat_id=? AND state=?;",
|
||||
paramsv![chat_id, MessageState::InFresh],
|
||||
"SELECT id FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;",
|
||||
paramsv![MessageState::InFresh, chat_id],
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -1703,10 +1861,11 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<(),
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET state=13
|
||||
WHERE chat_id=?
|
||||
AND state=10;",
|
||||
paramsv![chat_id],
|
||||
SET state=?
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?;",
|
||||
paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1842,7 +2001,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Vec<u32> {
|
||||
|
||||
pub async fn create_group_chat(
|
||||
context: &Context,
|
||||
verified: VerifiedStatus,
|
||||
protect: ProtectionStatus,
|
||||
chat_name: impl AsRef<str>,
|
||||
) -> Result<ChatId, Error> {
|
||||
let chat_name = improve_single_line_input(chat_name);
|
||||
@@ -1856,11 +2015,7 @@ pub async fn create_group_chat(
|
||||
context.sql.execute(
|
||||
"INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
paramsv![
|
||||
if verified != VerifiedStatus::Unverified {
|
||||
Chattype::VerifiedGroup
|
||||
} else {
|
||||
Chattype::Group
|
||||
},
|
||||
Chattype::Group,
|
||||
chat_name,
|
||||
grpid,
|
||||
time(),
|
||||
@@ -1884,6 +2039,12 @@ 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)
|
||||
}
|
||||
|
||||
@@ -2009,12 +2170,12 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
}
|
||||
} else {
|
||||
// else continue and send status mail
|
||||
if chat.typ == Chattype::VerifiedGroup
|
||||
if chat.is_protected()
|
||||
&& contact.is_verified(context).await != VerifiedStatus::BidirectVerified
|
||||
{
|
||||
error!(
|
||||
context,
|
||||
"Only bidirectional verified contacts can be added to verified groups."
|
||||
"Only bidirectional verified contacts can be added to protected chats."
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -2052,7 +2213,7 @@ async fn real_group_exists(context: &Context, chat_id: ChatId) -> bool {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT id FROM chats WHERE id=? AND (type=120 OR type=130);",
|
||||
"SELECT id FROM chats WHERE id=? AND type=120;",
|
||||
paramsv![chat_id],
|
||||
)
|
||||
.await
|
||||
@@ -2578,7 +2739,7 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a tuple of `(chatid, is_verified, blocked)`.
|
||||
/// Returns a tuple of `(chatid, is_protected, blocked)`.
|
||||
pub(crate) async fn get_chat_id_by_grpid(
|
||||
context: &Context,
|
||||
grpid: impl AsRef<str>,
|
||||
@@ -2586,14 +2747,16 @@ pub(crate) async fn get_chat_id_by_grpid(
|
||||
context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT id, blocked, type FROM chats WHERE grpid=?;",
|
||||
"SELECT id, blocked, protected 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 v = row.get::<_, Option<Chattype>>(2)?.unwrap_or_default();
|
||||
Ok((chat_id, v == Chattype::VerifiedGroup, b))
|
||||
let p = row
|
||||
.get::<_, Option<ProtectionStatus>>(2)?
|
||||
.unwrap_or_default();
|
||||
Ok((chat_id, p == ProtectionStatus::Protected, b))
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -2746,18 +2909,22 @@ 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(context: &Context, chat_id: ChatId, text: impl AsRef<str>) {
|
||||
pub(crate) async fn add_info_msg_with_cmd(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
text: impl AsRef<str>,
|
||||
cmd: SystemMessage,
|
||||
) -> Result<MsgId, Error> {
|
||||
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
|
||||
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 ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
|
||||
|
||||
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 (?,?,?, ?,?,?, ?,?,?);",
|
||||
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 (?,?,?, ?,?,?, ?,?,?, ?);",
|
||||
paramsv![
|
||||
chat_id,
|
||||
DC_CONTACT_ID_INFO,
|
||||
@@ -2767,22 +2934,25 @@ pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl
|
||||
MessageState::InNoticed,
|
||||
text.as_ref().to_string(),
|
||||
rfc724_mid,
|
||||
ephemeral_timer
|
||||
ephemeral_timer,
|
||||
param.to_string(),
|
||||
]
|
||||
).await {
|
||||
warn!(context, "Could not add info msg: {}", e);
|
||||
return;
|
||||
}
|
||||
).await?;
|
||||
|
||||
let row_id = context
|
||||
.sql
|
||||
.get_rowid(context, "msgs", "rfc724_mid", &rfc724_mid)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: MsgId::new(row_id),
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2875,7 +3045,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, VerifiedStatus::Unverified, "foo")
|
||||
let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
let added = add_contact_to_chat_ex(&t.ctx, chat_id, DC_CONTACT_ID_SELF, false)
|
||||
@@ -3261,7 +3431,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1000)).await;
|
||||
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
|
||||
let chat_id3 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -3306,7 +3476,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, VerifiedStatus::Unverified, "foo")
|
||||
let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -3349,7 +3519,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, VerifiedStatus::Unverified, "foo")
|
||||
let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).await.unwrap());
|
||||
@@ -3373,7 +3543,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, VerifiedStatus::Unverified, "foo")
|
||||
let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
// Initial
|
||||
@@ -3437,4 +3607,116 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,9 +362,7 @@ 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 || chat.typ == Chattype::VerifiedGroup)
|
||||
{
|
||||
if lastmsg.from_id != DC_CONTACT_ID_SELF && chat.typ == Chattype::Group {
|
||||
lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok();
|
||||
}
|
||||
|
||||
@@ -440,13 +438,13 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn test_try_load() {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
|
||||
let chat_id1 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat")
|
||||
let chat_id2 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "b chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat")
|
||||
let chat_id3 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "c chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -489,7 +487,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, VerifiedStatus::Unverified, "a chat")
|
||||
create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -546,7 +544,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, VerifiedStatus::Unverified, "a chat")
|
||||
let chat_id1 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
FetchExisting,
|
||||
|
||||
#[strum(props(default = "0"))]
|
||||
KeyGenType,
|
||||
|
||||
@@ -121,6 +124,8 @@ 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"))]
|
||||
|
||||
@@ -5,31 +5,46 @@ mod auto_outlook;
|
||||
mod read_url;
|
||||
mod server_params;
|
||||
|
||||
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 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 crate::config::Config;
|
||||
use crate::dc_tools::*;
|
||||
use crate::imap::Imap;
|
||||
use crate::login_param::{LoginParam, ServerLoginParam};
|
||||
use crate::message::Message;
|
||||
use crate::oauth2::*;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::{chat, e2ee, provider};
|
||||
use crate::{constants::*, job};
|
||||
use crate::{context::Context, param::Params};
|
||||
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
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 {
|
||||
@@ -50,18 +65,10 @@ 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(&progress)
|
||||
.inner_configure()
|
||||
.race(cancel_channel.recv().map(|_| {
|
||||
progress.p(0);
|
||||
progress!(self, 0);
|
||||
Ok(())
|
||||
}))
|
||||
.await;
|
||||
@@ -71,11 +78,11 @@ impl Context {
|
||||
res
|
||||
}
|
||||
|
||||
async fn inner_configure(&self, progress: &impl Progress) -> Result<()> {
|
||||
async fn inner_configure(&self) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let mut param = LoginParam::from_database(self, "").await;
|
||||
let success = configure(self, &mut param, progress).await;
|
||||
let success = configure(self, &mut param).await;
|
||||
self.set_config(Config::NotifyAboutWrongPw, None).await?;
|
||||
|
||||
if let Some(provider) = provider::get_provider_info(¶m.addr) {
|
||||
@@ -109,24 +116,21 @@ impl Context {
|
||||
Ok(_) => {
|
||||
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
progress.p(1000);
|
||||
progress!(self, 1000);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
progress.kill().await;
|
||||
emit_event!(
|
||||
progress!(
|
||||
self,
|
||||
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
|
||||
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),
|
||||
)
|
||||
}
|
||||
.await
|
||||
)
|
||||
);
|
||||
Err(err)
|
||||
}
|
||||
@@ -134,8 +138,8 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progress) -> Result<()> {
|
||||
progress.p(1);
|
||||
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
progress!(ctx, 1);
|
||||
|
||||
// Check basic settings.
|
||||
ensure!(!param.addr.is_empty(), "Please enter an email address.");
|
||||
@@ -159,12 +163,15 @@ async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progre
|
||||
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.p(10);
|
||||
progress!(ctx, 10);
|
||||
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password)
|
||||
.await
|
||||
.and_then(|e| e.parse().ok())
|
||||
@@ -175,7 +182,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progre
|
||||
.set_raw_config(ctx, "addr", Some(param.addr.as_str()))
|
||||
.await?;
|
||||
}
|
||||
progress.p(20);
|
||||
progress!(ctx, 20);
|
||||
}
|
||||
// no oauth? - just continue it's no error
|
||||
|
||||
@@ -184,7 +191,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progre
|
||||
let param_addr_urlencoded = utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string();
|
||||
|
||||
// Step 2: Autoconfig
|
||||
progress.p(200);
|
||||
progress!(ctx, 200);
|
||||
|
||||
let param_autoconfig;
|
||||
if param.imap.server.is_empty()
|
||||
@@ -202,13 +209,13 @@ async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progre
|
||||
param_autoconfig = Some(servers);
|
||||
} else {
|
||||
param_autoconfig =
|
||||
get_autoconfig(ctx, param, ¶m_domain, ¶m_addr_urlencoded, progress).await;
|
||||
get_autoconfig(ctx, param, ¶m_domain, ¶m_addr_urlencoded).await;
|
||||
}
|
||||
} else {
|
||||
param_autoconfig = None;
|
||||
}
|
||||
|
||||
progress.p(500);
|
||||
progress!(ctx, 500);
|
||||
|
||||
let servers = expand_param_vector(
|
||||
param_autoconfig.unwrap_or_else(|| {
|
||||
@@ -233,7 +240,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progre
|
||||
¶m_domain,
|
||||
);
|
||||
|
||||
progress.p(550);
|
||||
progress!(ctx, 550);
|
||||
|
||||
// Spawn SMTP configuration task
|
||||
let mut smtp = Smtp::new();
|
||||
@@ -274,7 +281,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progre
|
||||
}
|
||||
});
|
||||
|
||||
progress.p(600);
|
||||
progress!(ctx, 600);
|
||||
|
||||
// Configure IMAP
|
||||
let (_s, r) = async_std::sync::channel(1);
|
||||
@@ -300,13 +307,16 @@ async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progre
|
||||
}
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
progress.p(600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count);
|
||||
progress!(
|
||||
ctx,
|
||||
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
|
||||
);
|
||||
}
|
||||
if !imap_configured {
|
||||
bail!(nicer_configuration_error(ctx, errors).await);
|
||||
}
|
||||
|
||||
progress.p(850);
|
||||
progress!(ctx, 850);
|
||||
|
||||
// Wait for SMTP configuration
|
||||
match smtp_config_task.await {
|
||||
@@ -318,7 +328,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progre
|
||||
}
|
||||
}
|
||||
|
||||
progress.p(900);
|
||||
progress!(ctx, 900);
|
||||
|
||||
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
|
||||
|| ctx.get_config_bool(Config::MvboxMove).await;
|
||||
@@ -331,14 +341,14 @@ async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progre
|
||||
|
||||
drop(imap);
|
||||
|
||||
progress.p(910);
|
||||
progress!(ctx, 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.p(920);
|
||||
progress!(ctx, 920);
|
||||
|
||||
e2ee::ensure_secret_key_exists(ctx).await?;
|
||||
info!(ctx, "key generation completed");
|
||||
@@ -349,7 +359,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam, progress: &impl Progre
|
||||
)
|
||||
.await;
|
||||
|
||||
progress.p(940);
|
||||
progress!(ctx, 940);
|
||||
update_device_chats_handle.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -423,15 +434,14 @@ 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 p = 300;
|
||||
let mut progress = 300;
|
||||
for source in &sources {
|
||||
let res = source.fetch(ctx, param).await;
|
||||
progress.p(p);
|
||||
p += 10;
|
||||
progress!(ctx, progress);
|
||||
progress += 10;
|
||||
if let Ok(res) = res {
|
||||
return Some(res);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
//! # Constants
|
||||
use deltachat_derive::*;
|
||||
use lazy_static::lazy_static;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
|
||||
}
|
||||
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -145,7 +143,6 @@ pub enum Chattype {
|
||||
Undefined = 0,
|
||||
Single = 100,
|
||||
Group = 120,
|
||||
VerifiedGroup = 130,
|
||||
}
|
||||
|
||||
impl Default for Chattype {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use async_std::path::PathBuf;
|
||||
use deltachat_derive::*;
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
@@ -1053,9 +1053,7 @@ pub fn addr_normalize(addr: &str) -> &str {
|
||||
}
|
||||
|
||||
fn sanitize_name_and_addr(name: impl AsRef<str>, addr: impl AsRef<str>) -> (String, String) {
|
||||
lazy_static! {
|
||||
static ref ADDR_WITH_NAME_REGEX: Regex = Regex::new("(.*)<(.*)>").unwrap();
|
||||
}
|
||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.as_ref().is_empty() {
|
||||
|
||||
@@ -4,7 +4,7 @@ use sha2::{Digest, Sha256};
|
||||
|
||||
use mailparse::SingleInfo;
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
@@ -714,6 +714,43 @@ 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;
|
||||
@@ -868,6 +905,11 @@ 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;
|
||||
@@ -1149,29 +1191,18 @@ 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, chat_id_verified, _blocked) = chat::get_chat_id_by_grpid(context, &grpid)
|
||||
let (mut chat_id, _, _blocked) = chat::get_chat_id_by_grpid(context, &grpid)
|
||||
.await
|
||||
.unwrap_or((ChatId::new(0), false, Blocked::Not));
|
||||
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());
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
// check if the group does not exist but should be created
|
||||
@@ -1194,7 +1225,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_verified = if mime_parser.get(HeaderDef::ChatVerified).is_some() {
|
||||
let create_protected = 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
|
||||
{
|
||||
@@ -1202,9 +1233,9 @@ async fn create_or_lookup_group(
|
||||
let s = format!("{}. See 'Info' for more details", err);
|
||||
mime_parser.repl_msg_by_error(&s);
|
||||
}
|
||||
VerifiedStatus::Verified
|
||||
ProtectionStatus::Protected
|
||||
} else {
|
||||
VerifiedStatus::Unverified
|
||||
ProtectionStatus::Unprotected
|
||||
};
|
||||
|
||||
if !allow_creation {
|
||||
@@ -1217,11 +1248,22 @@ async fn create_or_lookup_group(
|
||||
&grpid,
|
||||
grpname.as_ref().unwrap(),
|
||||
create_blocked,
|
||||
create_verified,
|
||||
create_protected,
|
||||
)
|
||||
.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
|
||||
@@ -1273,7 +1315,10 @@ 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 {
|
||||
@@ -1458,7 +1503,7 @@ async fn create_or_lookup_adhoc_group(
|
||||
&grpid,
|
||||
grpname,
|
||||
create_blocked,
|
||||
VerifiedStatus::Unverified,
|
||||
ProtectionStatus::Unprotected,
|
||||
)
|
||||
.await;
|
||||
for &member_id in &member_ids {
|
||||
@@ -1475,20 +1520,17 @@ async fn create_group_record(
|
||||
grpid: impl AsRef<str>,
|
||||
grpname: impl AsRef<str>,
|
||||
create_blocked: Blocked,
|
||||
create_verified: VerifiedStatus,
|
||||
create_protected: ProtectionStatus,
|
||||
) -> ChatId {
|
||||
if context.sql.execute(
|
||||
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?);",
|
||||
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected) VALUES(?, ?, ?, ?, ?, ?);",
|
||||
paramsv![
|
||||
if VerifiedStatus::Unverified != create_verified {
|
||||
Chattype::VerifiedGroup
|
||||
} else {
|
||||
Chattype::Group
|
||||
},
|
||||
Chattype::Group,
|
||||
grpname.as_ref(),
|
||||
grpid.as_ref(),
|
||||
create_blocked,
|
||||
time(),
|
||||
create_protected,
|
||||
],
|
||||
).await
|
||||
.is_err()
|
||||
@@ -1640,6 +1682,17 @@ 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
|
||||
@@ -1729,7 +1782,7 @@ async fn check_verified_properties(
|
||||
}
|
||||
if !is_verified {
|
||||
bail!(
|
||||
"{} is not a member of this verified group",
|
||||
"{} is not a member of this protected chat",
|
||||
to_addr.to_string()
|
||||
);
|
||||
}
|
||||
@@ -2177,7 +2230,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, VerifiedStatus::Unverified, "foo")
|
||||
let group_id = chat::create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
chat::add_contact_to_chat(&t.ctx, group_id, bob_id).await;
|
||||
|
||||
102
src/dc_tools.rs
102
src/dc_tools.rs
@@ -2,20 +2,15 @@
|
||||
//! 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};
|
||||
@@ -29,94 +24,6 @@ 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)]
|
||||
@@ -810,10 +717,7 @@ where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn is_none_or_empty(&self) -> bool {
|
||||
match self {
|
||||
Some(s) if !s.as_ref().is_empty() => false,
|
||||
_ => true,
|
||||
}
|
||||
!matches!(self, Some(s) if !s.as_ref().is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
//!
|
||||
//! A module to remove HTML tags from the email text
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use once_cell::sync::Lazy;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
lazy_static! {
|
||||
static ref LINE_RE: regex::Regex = regex::Regex::new(r"(\r?\n)+").unwrap();
|
||||
}
|
||||
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
|
||||
struct Dehtml {
|
||||
strbuilder: String,
|
||||
@@ -24,16 +22,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) -> String {
|
||||
pub fn dehtml(buf: &str) -> Option<String> {
|
||||
let s = dehtml_quick_xml(buf);
|
||||
if !s.trim().is_empty() {
|
||||
return s;
|
||||
return Some(s);
|
||||
}
|
||||
let s = dehtml_manually(buf);
|
||||
if !s.trim().is_empty() {
|
||||
return s;
|
||||
return Some(s);
|
||||
}
|
||||
buf.to_string()
|
||||
None
|
||||
}
|
||||
|
||||
pub fn dehtml_quick_xml(buf: &str) -> String {
|
||||
@@ -222,21 +220,23 @@ 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), true).0, output);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
let plain = dehtml(html).unwrap();
|
||||
|
||||
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);
|
||||
let plain = dehtml(html).unwrap();
|
||||
|
||||
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);
|
||||
let plain = dehtml(html).unwrap();
|
||||
|
||||
assert_eq!(plain, "text *bold*<>");
|
||||
}
|
||||
@@ -262,7 +262,7 @@ mod tests {
|
||||
let html =
|
||||
"<>"'& äÄöÖüÜß fooÆçÇ ♦‎‏‌&noent;‍";
|
||||
|
||||
let plain = dehtml(html);
|
||||
let plain = dehtml(html).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
plain,
|
||||
@@ -285,7 +285,7 @@ mod tests {
|
||||
</body>
|
||||
</html>
|
||||
"##;
|
||||
let txt = dehtml(input);
|
||||
let txt = dehtml(input).unwrap();
|
||||
assert_eq!(txt.trim(), "lots of text");
|
||||
}
|
||||
}
|
||||
|
||||
70
src/e2ee.rs
70
src/e2ee.rs
@@ -57,12 +57,9 @@ 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 verified groups.
|
||||
/// Autocrypt Level 1, version 1.1) and for messages sent in protected 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,
|
||||
@@ -84,7 +81,11 @@ impl EncryptHelper {
|
||||
match peerstate.prefer_encrypt {
|
||||
EncryptPreference::NoPreference => {}
|
||||
EncryptPreference::Mutual => prefer_encrypt_count += 1,
|
||||
EncryptPreference::Reset => return Ok(false),
|
||||
EncryptPreference::Reset => {
|
||||
if !e2ee_guaranteed {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
None => {
|
||||
@@ -234,9 +235,9 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail
|
||||
}
|
||||
}
|
||||
|
||||
async fn decrypt_if_autocrypt_message<'a>(
|
||||
async fn decrypt_if_autocrypt_message(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'a>,
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
ret_valid_signatures: &mut HashSet<Fingerprint>,
|
||||
@@ -510,4 +511,59 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
55
src/imex.rs
55
src/imex.rs
@@ -503,10 +503,20 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
);
|
||||
|
||||
let backup_file = File::open(backup_to_import).await?;
|
||||
let file_size = backup_file.metadata().await?.len();
|
||||
let archive = Archive::new(backup_file);
|
||||
|
||||
let mut entries = archive.entries()?;
|
||||
while let Some(file) = entries.next().await {
|
||||
let f = &mut file?;
|
||||
|
||||
let current_pos = f.raw_file_position();
|
||||
let progress = 1000 * current_pos / file_size;
|
||||
if progress > 10 && progress < 1000 {
|
||||
// We already emitted ImexProgress(10) above
|
||||
context.emit_event(EventType::ImexProgress(progress as usize));
|
||||
}
|
||||
|
||||
if f.path()?.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
|
||||
// async_tar can't unpack to a specified file name, so we just unpack to the blobdir and then move the unpacked file.
|
||||
f.unpack_in(context.get_blobdir()).await?;
|
||||
@@ -515,7 +525,6 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
|
||||
context.get_dbfile(),
|
||||
)
|
||||
.await?;
|
||||
context.emit_event(EventType::ImexProgress(400)); // Just guess the progress, we at least have the dbfile by now
|
||||
} else {
|
||||
// async_tar will unpack to blobdir/BLOBS_BACKUP_NAME, so we move the file afterwards.
|
||||
f.unpack_in(context.get_blobdir()).await?;
|
||||
@@ -715,11 +724,32 @@ async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<(
|
||||
.append_path_with_name(context.get_dbfile(), DBFILE_BACKUP_NAME)
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::ImexProgress(500));
|
||||
let read_dir: Vec<_> = fs::read_dir(context.get_blobdir()).await?.collect().await;
|
||||
let count = read_dir.len();
|
||||
let mut written_files = 0;
|
||||
|
||||
builder
|
||||
.append_dir_all(BLOBS_BACKUP_NAME, context.get_blobdir())
|
||||
.await?;
|
||||
for entry in read_dir.into_iter() {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
if !entry.file_type().await?.is_file() {
|
||||
warn!(
|
||||
context,
|
||||
"Export: Found dir entry {} that is not a file, ignoring",
|
||||
name.to_string_lossy()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let mut file = File::open(entry.path()).await?;
|
||||
let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(name);
|
||||
builder.append_file(path_in_archive, &mut file).await?;
|
||||
|
||||
written_files += 1;
|
||||
let progress = 1000 * written_files / count;
|
||||
if progress > 10 && progress < 1000 {
|
||||
// We already emitted ImexProgress(10) above
|
||||
emit_event!(context, EventType::ImexProgress(progress));
|
||||
}
|
||||
}
|
||||
|
||||
builder.finish().await?;
|
||||
Ok(())
|
||||
@@ -1054,6 +1084,21 @@ mod tests {
|
||||
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 = "$BLOBDIR";
|
||||
assert!(imex(&context.ctx, ImexMode::ExportSelfKeys, Some(blobdir))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
|
||||
assert!(imex(&context.ctx, ImexMode::ImportSelfKeys, Some(blobdir))
|
||||
.await
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_setup_code() {
|
||||
let norm = normalize_setup_code("123422343234423452346234723482349234");
|
||||
|
||||
24
src/job.rs
24
src/job.rs
@@ -636,17 +636,19 @@ impl Job {
|
||||
add_all_recipients_as_contacts(context, imap, Config::ConfiguredMvboxFolder).await;
|
||||
add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).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;
|
||||
};
|
||||
if context.get_config_bool(Config::FetchExisting).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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
info!(context, "Done fetching existing messages.");
|
||||
|
||||
@@ -431,11 +431,9 @@ mod tests {
|
||||
use std::error::Error;
|
||||
|
||||
use async_std::sync::Arc;
|
||||
use lazy_static::lazy_static;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
lazy_static! {
|
||||
static ref KEYPAIR: KeyPair = alice_keypair();
|
||||
}
|
||||
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
|
||||
|
||||
#[test]
|
||||
fn test_from_armored_string() {
|
||||
|
||||
@@ -544,9 +544,7 @@ impl Message {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let contact = if self.from_id != DC_CONTACT_ID_SELF as u32
|
||||
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
|
||||
{
|
||||
let contact = if self.from_id != DC_CONTACT_ID_SELF as u32 && chat.typ == Chattype::Group {
|
||||
Contact::get_by_id(context, self.from_id).await.ok()
|
||||
} else {
|
||||
None
|
||||
@@ -591,6 +589,10 @@ 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
|
||||
@@ -976,7 +978,7 @@ impl Lot {
|
||||
);
|
||||
self.text1_meaning = Meaning::Text1Self;
|
||||
}
|
||||
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
|
||||
} else if chat.typ == Chattype::Group {
|
||||
if msg.is_info() || contact.is_none() {
|
||||
self.text1 = None;
|
||||
self.text1_meaning = Meaning::None;
|
||||
@@ -996,16 +998,23 @@ impl Lot {
|
||||
}
|
||||
}
|
||||
|
||||
self.text2 = Some(
|
||||
get_summarytext_by_raw(
|
||||
msg.viewtype,
|
||||
msg.text.as_ref(),
|
||||
&msg.param,
|
||||
SUMMARY_CHARACTERS,
|
||||
context,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
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.timestamp = msg.get_timestamp();
|
||||
self.state = msg.state.into();
|
||||
@@ -1187,6 +1196,7 @@ 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" => (
|
||||
@@ -1661,7 +1671,7 @@ pub(crate) async fn handle_ndn(
|
||||
if let Ok((msg_id, chat_id, chat_type)) = res {
|
||||
set_msg_failed(context, msg_id, error).await;
|
||||
|
||||
if chat_type == Chattype::Group || chat_type == Chattype::VerifiedGroup {
|
||||
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;
|
||||
|
||||
@@ -233,7 +233,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
fn is_e2ee_guaranteed(&self) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat } => {
|
||||
if chat.typ == Chattype::VerifiedGroup {
|
||||
if chat.is_protected() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
fn min_verified(&self) -> PeerstateVerifiedStatus {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat } => {
|
||||
if chat.typ == Chattype::VerifiedGroup {
|
||||
if chat.is_protected() {
|
||||
PeerstateVerifiedStatus::BidirectVerified
|
||||
} else {
|
||||
PeerstateVerifiedStatus::Unverified
|
||||
@@ -268,7 +268,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
fn should_force_plaintext(&self) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat } => {
|
||||
if chat.typ == Chattype::VerifiedGroup {
|
||||
if chat.is_protected() {
|
||||
false
|
||||
} else {
|
||||
self.msg
|
||||
@@ -345,7 +345,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
.stock_str(StockMessage::AcSetupMsgSubject)
|
||||
.await
|
||||
.into_owned()
|
||||
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
|
||||
} else if chat.typ == Chattype::Group {
|
||||
let re = if self.in_reply_to.is_empty() {
|
||||
""
|
||||
} else {
|
||||
@@ -708,11 +708,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
||||
let mut placeholdertext = None;
|
||||
let mut meta_part = None;
|
||||
|
||||
if chat.typ == Chattype::VerifiedGroup {
|
||||
if chat.is_protected() {
|
||||
protected_headers.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
|
||||
}
|
||||
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
|
||||
if chat.typ == Chattype::Group {
|
||||
protected_headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
|
||||
|
||||
let encoded = encode_words(&chat.name);
|
||||
@@ -846,6 +846,18 @@ 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(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +86,10 @@ pub enum SystemMessage {
|
||||
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged = 10,
|
||||
|
||||
// Chat protection state changed
|
||||
ChatProtectionEnabled = 11,
|
||||
ChatProtectionDisabled = 12,
|
||||
}
|
||||
|
||||
impl Default for SystemMessage {
|
||||
@@ -123,6 +127,7 @@ 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;
|
||||
@@ -218,6 +223,7 @@ 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)?;
|
||||
|
||||
@@ -253,6 +259,10 @@ 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(())
|
||||
@@ -704,12 +714,17 @@ 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)
|
||||
dehtml(&decoded_data).unwrap_or_else(|| {
|
||||
dehtml_failed = true;
|
||||
decoded_data.clone()
|
||||
})
|
||||
} else {
|
||||
decoded_data.clone()
|
||||
};
|
||||
@@ -739,8 +754,9 @@ impl MimeMessage {
|
||||
(simplified_txt, top_quote)
|
||||
};
|
||||
|
||||
if !simplified_txt.is_empty() {
|
||||
if !simplified_txt.is_empty() || simplified_quote.is_some() {
|
||||
let mut part = Part::default();
|
||||
part.dehtlm_failed = dehtml_failed;
|
||||
part.typ = Viewtype::Text;
|
||||
part.mimetype = Some(mime_type);
|
||||
part.msg = simplified_txt;
|
||||
@@ -848,6 +864,7 @@ 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());
|
||||
@@ -982,11 +999,21 @@ 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) -> Option<()> {
|
||||
async fn heuristically_parse_ndn(&mut self, context: &Context) {
|
||||
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")
|
||||
@@ -994,9 +1021,8 @@ impl MimeMessage {
|
||||
false
|
||||
};
|
||||
if maybe_ndn && self.failure_report.is_none() {
|
||||
lazy_static! {
|
||||
static ref RE: regex::Regex = regex::Regex::new(r"Message-ID:(.*)").unwrap();
|
||||
}
|
||||
static RE: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r"Message-ID:(.*)").unwrap());
|
||||
for captures in self
|
||||
.parts
|
||||
.iter()
|
||||
@@ -1016,7 +1042,6 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
}
|
||||
None // Always return None, we just return anything so that we can use the '?' operator.
|
||||
}
|
||||
|
||||
/// Handle reports
|
||||
@@ -1185,6 +1210,7 @@ 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
|
||||
@@ -1866,6 +1892,55 @@ 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;
|
||||
@@ -2159,4 +2234,36 @@ 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, "");
|
||||
}
|
||||
}
|
||||
|
||||
41
src/pgp.rs
41
src/pgp.rs
@@ -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 lazy_static::lazy_static;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
#[test]
|
||||
fn test_split_armored_data_1() {
|
||||
@@ -449,26 +449,29 @@ mod tests {
|
||||
/// The original text of [CTEXT_SIGNED]
|
||||
static CLEARTEXT: &[u8] = b"This is a test";
|
||||
|
||||
lazy_static! {
|
||||
/// Initialised [TestKeys] for tests.
|
||||
static ref KEYS: TestKeys = TestKeys::new();
|
||||
/// Initialised [TestKeys] for tests.
|
||||
static KEYS: Lazy<TestKeys> = Lazy::new(TestKeys::new);
|
||||
|
||||
/// 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, 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, 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()
|
||||
};
|
||||
}
|
||||
/// 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()
|
||||
});
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_signed() {
|
||||
|
||||
1509
src/provider/data.rs
1509
src/provider/data.rs
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
@@ -111,16 +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 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"
|
||||
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 += " oauth2_authorizer: " + oauth2 + ",\n"
|
||||
provider += "});\n\n"
|
||||
else:
|
||||
raise TypeError("SMTP and IMAP must be specified together or left out both")
|
||||
|
||||
@@ -129,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
|
||||
@@ -164,18 +164,16 @@ if __name__ == "__main__":
|
||||
"use crate::provider::UsernamePattern::*;\n"
|
||||
"use crate::provider::*;\n"
|
||||
"use std::collections::HashMap;\n\n"
|
||||
"lazy_static::lazy_static! {\n\n")
|
||||
"use once_cell::sync::Lazy;\n\n")
|
||||
|
||||
process_dir(sys.argv[1])
|
||||
|
||||
out_all += " pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [\n"
|
||||
out_all += "pub static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| [\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 ref PROVIDER_UPDATED: chrono::NaiveDate = "\
|
||||
"chrono::NaiveDate::from_ymd("+str(now.year)+", "+str(now.month)+", "+str(now.day)+");\n"
|
||||
|
||||
out_all += "}"
|
||||
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"
|
||||
|
||||
print(out_all)
|
||||
|
||||
12
src/qr.rs
12
src/qr.rs
@@ -1,6 +1,6 @@
|
||||
//! # QR code module
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use once_cell::sync::Lazy;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -358,12 +358,10 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Lot {
|
||||
Lot::from_address(context, name, addr).await
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
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());
|
||||
|
||||
/// Extract address for the matmsg scheme.
|
||||
///
|
||||
|
||||
@@ -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_verified, _blocked)) => break chatid,
|
||||
Ok((chatid, _is_protected, _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_verified_group will always be
|
||||
// This is buggy, is_protected_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_verified_group, _) = chat::get_chat_id_by_grpid(context, &group_id)
|
||||
let (_, is_protected_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_verified_group
|
||||
is_protected_group
|
||||
} else {
|
||||
// setup contact is always encrypted
|
||||
true
|
||||
@@ -1102,6 +1102,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
@@ -1328,7 +1329,7 @@ mod tests {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chatid = chat::create_group_chat(&alice.ctx, VerifiedStatus::Verified, "the chat")
|
||||
let chatid = chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1427,6 +1428,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_verified());
|
||||
assert!(bob_chat.is_protected());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,25 +71,20 @@ 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 original_lines = &lines;
|
||||
|
||||
let lines = remove_message_footer(lines);
|
||||
let (lines, top_quote) = remove_top_quote(lines);
|
||||
let original_lines = &lines;
|
||||
let lines = remove_message_footer(lines);
|
||||
|
||||
let text = if is_chat_message {
|
||||
render_message(lines, false, false)
|
||||
render_message(lines, false)
|
||||
} else {
|
||||
let (lines, has_nonstandard_footer) = remove_nonstandard_footer(lines);
|
||||
let (lines, has_bottom_quote) = remove_bottom_quote(lines);
|
||||
|
||||
if lines.iter().all(|it| it.trim().is_empty()) {
|
||||
render_message(original_lines, false, false)
|
||||
render_message(original_lines, false)
|
||||
} else {
|
||||
render_message(
|
||||
lines,
|
||||
top_quote.is_some(),
|
||||
has_nonstandard_footer || has_bottom_quote,
|
||||
)
|
||||
render_message(lines, has_nonstandard_footer || has_bottom_quote)
|
||||
}
|
||||
};
|
||||
(text, is_forwarded, top_quote)
|
||||
@@ -173,11 +168,8 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_message(lines: &[&str], is_cut_at_begin: bool, is_cut_at_end: bool) -> String {
|
||||
fn render_message(lines: &[&str], 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;
|
||||
@@ -200,7 +192,7 @@ fn render_message(lines: &[&str], is_cut_at_begin: bool, is_cut_at_end: bool) ->
|
||||
pending_linebreaks = 1
|
||||
}
|
||||
}
|
||||
if is_cut_at_end && (!is_cut_at_begin || !empty_body) {
|
||||
if is_cut_at_end && !empty_body {
|
||||
ret += " [...]";
|
||||
}
|
||||
// redo escaping done by escape_message_footer_marks()
|
||||
|
||||
16
src/sql.rs
16
src/sql.rs
@@ -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
|
||||
// the index is used to speed up get_fresh_msg_cnt() (see comment there for more details) and marknoticed_chat()
|
||||
sql.execute(
|
||||
"CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);",
|
||||
paramsv![],
|
||||
@@ -1368,6 +1368,20 @@ 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)
|
||||
|
||||
38
src/stock.rs
38
src/stock.rs
@@ -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,6 +14,7 @@ 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
|
||||
///
|
||||
@@ -233,6 +234,16 @@ 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,
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -398,16 +409,27 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
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? {
|
||||
/// 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 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// create saved-messages chat;
|
||||
// we do this only once, if the user has deleted the chat, he can recreate it manually.
|
||||
// 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)
|
||||
if !self.sql.get_raw_config_bool(&self, "self-chat-added").await {
|
||||
self.sql
|
||||
.set_raw_config_bool(&self, "self-chat-added", true)
|
||||
|
||||
@@ -9,13 +9,15 @@ use async_std::path::PathBuf;
|
||||
use async_std::sync::RwLock;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::chat;
|
||||
use crate::chat::{ChatId, ChatItem};
|
||||
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};
|
||||
|
||||
@@ -187,6 +189,19 @@ 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.
|
||||
|
||||
110
tests/stress.rs
110
tests/stress.rs
@@ -1,110 +0,0 @@
|
||||
//! 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;
|
||||
}
|
||||
Reference in New Issue
Block a user