Compare commits

..

68 Commits

Author SHA1 Message Date
holger krekel
ff34ed6054 try add a little testing of imex events 2020-10-19 18:48:55 +02:00
Hocuri
8aaea09011 Add progress for backup import/export 2020-10-19 15:38:25 +02:00
Hocuri
c005f756d6 Do not allow to save drafts in non-writeable chats, fix #1986 (#1997) 2020-10-19 13:15:48 +02:00
Hocuri
3c6d52842e Doc comments are show in HTML documentation.
This is not a proper documentation, just a note on implementation.
2020-10-19 13:07:55 +02:00
Hocuri
4d2542cee5 Don't show HTML if there is no content and there is a file attached
Fix https://github.com/deltachat/deltachat-core-rust/issues/1982
2020-10-19 13:07:55 +02:00
B. Petersen
9bf8799484 fix processing of protection-changed messages
by #2001, processing of protection-enabled and protection-disabled
messages is only done if verification-checks pass.

unfortunately, these verification checks
were only applied on already protected chats,
so that enabling via a protection-enabled message was not possible.

this is fixed by this pr.
moreover, when inner_set_protection() fails,
the error is shown in the chat.
2020-10-19 13:03:56 +02:00
B. Petersen
e15372531e skip protection enabled/disabled messages where they did not appear in the past with verified-groups
this avoids confusion - as long the current core
does not communicate with a UI that enables/disables protection,
everything looks just the same as in the past :)
2020-10-19 13:02:24 +02:00
link2xt
0ae9443e22 Update sanitize-filename (#2013) 2020-10-19 12:14:05 +02:00
Alexander Krotov
67cddedf7e Switch from lazy_static to once_cell 2020-10-18 15:47:21 +03:00
bjoern
bf72ae4ccc Merge pull request #2007 from deltachat/tweak-protection-msg
do not say, a concrete user has enabled protection if we do not really know
2020-10-17 22:20:36 +02:00
B. Petersen
c7863c67bf refine test for add_info_msg() and add_info_msg_with_cmd() 2020-10-17 22:02:55 +02:00
B. Petersen
3d108fedc4 set message-info-type also for unpromoted protect/unprotect messages so that UI can show a shield-icon or sth. line that 2020-10-17 21:31:09 +02:00
B. Petersen
546e8dedce allow adding info-message with defined commands, add tests for that and for set_protection() 2020-10-17 21:27:41 +02:00
Alexander Krotov
64cd48a4e1 Fix nightly clippy warnings 2020-10-17 15:24:09 +03:00
bjoern
2e118b773e Merge pull request #2009 from deltachat/fix-link
fix link to Protected Headers draft
2020-10-17 13:34:23 +02:00
B. Petersen
6206c82ee5 fix link to Protected Headers draft 2020-10-17 13:27:16 +02:00
B. Petersen
e7736138a8 clarify 'promote' parameter in add_protection_msg() 2020-10-17 11:52:56 +02:00
B. Petersen
78b44cb4d0 do not say a concrete user has enabled protection if we do not really know 2020-10-17 11:46:46 +02:00
bjoern
0a9a2394d8 Merge pull request #2001 from deltachat/protect-one-to-one
check protection properties for all chats, allow missing Chat-Verified header
2020-10-16 23:02:41 +02:00
bjoern
6e37c1442e Merge pull request #2006 from deltachat/update-provider-db-2020-10-16-ii
update provider-db, second time today
2020-10-16 22:58:28 +02:00
B. Petersen
c323798386 update provider-db, second time today 2020-10-16 21:43:18 +02:00
Hocuri
b8e98c0b81 Also add missing suffix (#1973) 2020-10-16 18:42:21 +02:00
B. Petersen
7a82fd4bbd update provider-db
ran `./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs`
to pull in fetch_existing setting for nauta.
2020-10-16 16:19:07 +02:00
B. Petersen
0a300da347 rename option 'Prefetch' to 'FetchExisting' and limit it to that 2020-10-16 15:28:41 +02:00
B. Petersen
aa26c52813 add prefetch config value 2020-10-16 15:28:41 +02:00
Alexander Krotov
19697e255e Parse normal MUA messages consisting of only a quote 2020-10-16 14:57:02 +02:00
Hocuri
07e4762f71 Call update_device_chats automatically during configure (#1957)
update_device_chats() takes about 2 seconds on a modern device (Android) because the
welcome image file has to be written to the disk as a blob. The problem
was that this was done after the progress bar had vanished and before
anything else happened so that I thought that something had gone wrong
multiple times.

The UIs have to remove update_device_chats(), too..
2020-10-15 17:43:22 +02:00
B. Petersen
6ec743f8b1 make clippy happy 2020-10-15 16:29:21 +02:00
B. Petersen
010be693e1 check protected properties for all chats
before, checks were done for groups only.
moreover, apply protection-changes only when the check passes.
2020-10-15 16:04:43 +02:00
B. Petersen
d8a7a178c2 skip check for Chat-Verified on normal messages
this would exclude non-deltas from protected-chats
(where they could participate in verified-groups in the past)
and makes migration a bit harder when using a fuzzy multi-device-setup.

however, the test still takes place
when a group is created or protection is enabled/disabled.
2020-10-15 15:19:25 +02:00
B. Petersen
18e9073bfe add unit test for should_encrypt() 2020-10-15 06:45:57 +02:00
B. Petersen
0032468a87 remove now inaccurate comment 2020-10-15 06:45:57 +02:00
B. Petersen
7e793a518c priorize e2ee_guaranteed over Reset
should_encrypt() shall return Ok(false)
on peerstate=EncryptPreference::Reset
only for opportunistic groups.

for verified/protected groups (e2ee_guaranteed set),
Ok(true) or Error() should be returned.

this bug was introduced by #1946 (Require quorum to enable encryption).
2020-10-15 06:45:57 +02:00
bjoern
e5b0194e8c Merge pull request #1987 from deltachat/name-quote-only-drafts
name quote-only drafts as such in the summary
2020-10-14 23:23:03 +02:00
holger krekel
13055b9c87 use new imap-proto/async-imap 0.4.1 to fix #1834 2020-10-14 15:41:23 +03:00
jikstra
5661e0b8f1 Add test to verify exporting and importing the secret key works 2020-10-14 06:46:25 +02:00
Alexander Krotov
1672905c71 Remove outdated tests/stress.rs
All useful code has already been moved to relevant modules.
2020-10-14 05:58:47 +02:00
B. Petersen
d13d62105a name quote-only drafts as such in the summary
a draft may contain only a quote,
without any text set yet.

these drafts cannot be sent, however, appear in the summary -
currently with the summary-text "", which results to sth.
as "Draft: " - which looks like an error or at least a bit weird.

this pr sets the summary text to "Reply" - similar to "Image", "Video" etc. -
the UI just expects some text here, not an empty string.

the result are summaries as "Draft: Reply" on all UIs -
which, btw. is also roughly the same what Signal does in this case.
2020-10-13 21:35:29 +02:00
bjoern
0b80b81129 Merge pull request #1991 from deltachat/remove-DC_STR_COUNT
remove DC_STR_COUNT
2020-10-13 21:32:48 +02:00
B. Petersen
9b72aba8e3 remove DC_STR_COUNT
the constant comes from c-core
and was used to define the size of the string-array that time.

this is no longer needed in core
and also the UIs seems not to use it
(they will treat DC_STR* like an id,
there should be no need to know the max. DC_STR* value)
2020-10-13 18:18:34 +02:00
Alexander Krotov
1f24c5f8a4 Merge "protected groups" branch into master 2020-10-13 18:44:31 +03:00
B. Petersen
7f882a6406 less duplicate code on calling inner_set_protection() 2020-10-13 14:59:29 +02:00
B. Petersen
50f3af58f8 remove dc_chat_is_verified() 2020-10-13 14:59:28 +02:00
B. Petersen
8425e23d82 move get_protection_msg() to stock.rs as stock_protection_msg() 2020-10-13 14:59:28 +02:00
B. Petersen
9fc6bbf41f tweak error texts 2020-10-13 14:59:28 +02:00
B. Petersen
1e2e042244 clarify that signature of add_protection_msg() takes chat-promoted parameter 2020-10-13 14:59:28 +02:00
B. Petersen
03d86360d6 details docstring, thanks @flub 2020-10-13 14:59:28 +02:00
B. Petersen
4eb8d3fef6 tweak documentation towards https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text - thanks @flub 2020-10-13 14:59:28 +02:00
B. Petersen
da727740ab use warn! for ffi-usage-errors 2020-10-13 14:59:28 +02:00
bjoern
3a993a4b77 Update src/chat.rs
Co-authored-by: Floris Bruynooghe <flub@devork.be>
2020-10-13 14:59:27 +02:00
B. Petersen
45dae1ff0c protect against attackers dropping the protect-this-chat message by not showing unprotected messages directly; this is done by checking the Chat-Verified flag on each incoming message. moreover, make sure, the flag is signed+encrypted (it must be read from the protected headers). 2020-10-13 14:59:27 +02:00
B. Petersen
f144426bf5 use 'unreachable' instead of 'panic' 2020-10-13 14:59:27 +02:00
B. Petersen
e447bdc0c3 simplify code-path 2020-10-13 14:59:27 +02:00
B. Petersen
c1768bb311 add dc_msg_get_info_type() to allow drawing a shield in the uis 2020-10-13 14:59:27 +02:00
B. Petersen
66cb3d4358 fix ffi and bindings, error is already logged in core 2020-10-13 14:59:27 +02:00
B. Petersen
47f4f2bd08 use language of receiver for protection-messages, show correct sender 2020-10-13 14:59:26 +02:00
B. Petersen
12cf89735c handling incoming protection-changes messages, always add info-msg 2020-10-13 14:59:26 +02:00
B. Petersen
d240bbcd07 if a message is replaced by an error, this also removes special is_system_message states 2020-10-13 14:59:26 +02:00
B. Petersen
5e07a36cd2 add/send info-message on protection changes 2020-10-13 14:59:26 +02:00
B. Petersen
49b5962568 add set_chat_protection() api 2020-10-13 14:59:26 +02:00
B. Petersen
a7998c190c remove DC_CHAT_TYPE_VERIFIED_GROUP resp. Chattype::VerifiedGroup 2020-10-13 14:59:26 +02:00
B. Petersen
b8a55f3aa4 replace chat.is_verified() by chat.is_protected() 2020-10-13 14:59:25 +02:00
B. Petersen
ab8bf3c2f3 use ProtectionStatus to create chats 2020-10-13 14:59:25 +02:00
B. Petersen
d05dd977d9 migrate database
add 'protected' row in chats table,
convert old verified-groups to 'protected'
2020-10-13 14:59:25 +02:00
Alexander Krotov
8b3494b5c1 Do not display [...] after non-chat quotes
Top quotes are displayed as quotes for non-chat mails, so [...] used to
indicate there was a quote is not needed.
2020-10-12 21:55:22 +03:00
bjoern
d9a45eb931 Merge pull request #1981 from deltachat/notice-on-answer
basic DC_EVENT_MSGS_NOTICED multi-device-support
2020-10-12 17:58:32 +02:00
B. Petersen
c2b222e6a5 emit DC_EVENT_MSGS_NOTICED when a chat was answered on another device 2020-10-10 12:50:23 +02:00
B. Petersen
4c8e6ef495 use combined index (state, hidden, chat_id) to speed up marknoticed_chat() 2020-10-10 00:32:45 +02:00
39 changed files with 2064 additions and 1240 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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" }

View File

@@ -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") */
/*
* @}

View File

@@ -25,7 +25,7 @@ use async_std::task::{block_on, spawn};
use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::accounts::Accounts;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
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() {

View File

@@ -4,7 +4,7 @@ use std::str::FromStr;
use anyhow::{bail, ensure};
use async_std::path::Path;
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility};
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?;

View File

@@ -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()

View File

@@ -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.

View File

@@ -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.

View File

@@ -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():

View File

@@ -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 = []

View File

@@ -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")

View File

@@ -32,7 +32,7 @@ Messages SHOULD be encrypted by the
`prefer-encrypt=mutual` MAY be set by default.
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
by the [Protected Headers](https://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

View File

@@ -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
}
}

View File

@@ -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();

View File

@@ -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"))]

View File

@@ -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(&param.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, &param.addr, &param.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(&param.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, &param_domain, &param_addr_urlencoded, progress).await;
get_autoconfig(ctx, param, &param_domain, &param_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
&param_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);
}

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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())
}
}

View File

@@ -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 =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let plain = dehtml(html);
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");
}
}

View File

@@ -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());
}
}

View File

@@ -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");

View File

@@ -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.");

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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(),
));
}
_ => {}
}

View File

@@ -3,9 +3,9 @@ use std::future::Future;
use std::pin::Pin;
use deltachat_derive::{FromSql, ToSql};
use lazy_static::lazy_static;
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use once_cell::sync::Lazy;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
@@ -86,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, "");
}
}

View File

@@ -381,7 +381,7 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
mod tests {
use super::*;
use crate::test_utils::*;
use 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() {

File diff suppressed because it is too large Load Diff

View File

@@ -42,8 +42,8 @@ def process_config_defaults(data):
config_defaults = data.get("config_defaults", "")
for key in config_defaults:
value = str(config_defaults[key])
defaults += " ConfigDefault { key: Config::" + camel(key) + ", value: \"" + value + "\" },\n"
defaults += " ])"
defaults += " ConfigDefault { key: Config::" + camel(key) + ", value: \"" + value + "\" },\n"
defaults += " ])"
return defaults
@@ -66,7 +66,7 @@ def process_data(data, file):
raise TypeError("domain used twice: " + domain)
domains_dict[domain] = True
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
comment += domain + ", "
@@ -96,7 +96,7 @@ def process_data(data, file):
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
raise TypeError("bad username pattern")
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern + " },\n")
config_defaults = process_config_defaults(data)
@@ -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)

View File

@@ -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.
///

View File

@@ -348,7 +348,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
let bob = context.bob.read().await;
let grpid = bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap();
match chat::get_chat_id_by_grpid(context, grpid).await {
Ok((chatid, _is_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());
}
}

View File

@@ -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()

View File

@@ -1360,7 +1360,7 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
}
if dbversion < 68 {
info!(context, "[migration] v68");
// the index is used to speed up get_fresh_msg_cnt(), see comment there for more details
// 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)

View File

@@ -5,8 +5,8 @@ use std::borrow::Cow;
use strum::EnumProperty;
use strum_macros::EnumProperty;
use crate::blob::BlobObject;
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::*;
use crate::context::Context;
@@ -14,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)

View File

@@ -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.

View File

@@ -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;
}