Compare commits

...

121 Commits

Author SHA1 Message Date
Simon Laux
6161b50bc7 attachments 2020-05-07 02:09:01 +02:00
Simon Laux
cbd45bc0ea cargo fmt 2020-05-07 01:01:44 +02:00
Simon Laux
b3eaf6730f store chat image by filename,
same as all other blobs
also deduplicate blobs before saving
2020-05-07 00:34:23 +02:00
Simon Laux
c3be1e3163 fix: image avatars went missing 2020-05-07 00:28:58 +02:00
Simon Laux
2e60380803 fix css bug 2020-05-07 00:25:09 +02:00
Simon Laux
108826d4af hide avatar icon and author name on outgoing
messages
2020-05-07 00:22:06 +02:00
Simon Laux
d36d6bc87c add header with chat name and avatar 2020-05-07 00:16:55 +02:00
Simon Laux
7b5c946c82 add a todo point to css file 2020-04-15 22:48:08 +02:00
Simon Laux
8665a3f8ad write zip file, pack blobs into it
and copy some styles over from desktop
2020-04-15 22:45:20 +02:00
Simon Laux
c99131f551 change string style 2020-04-07 20:59:32 +02:00
Simon Laux
9f24d57835 checkpoint 3 2020-04-07 20:59:29 +02:00
Simon Laux
817050260f second checkpoint 2020-04-07 20:43:14 +02:00
Simon Laux
e3b8b64c16 just a backup commit, nothing works yet 2020-04-07 20:43:14 +02:00
Alexander Krotov
d31265895d Update rPGP to 0.5.2 2020-04-03 12:57:13 +03:00
bjoern
6e35a879a3 Merge pull request #1375 from deltachat/robust-hide-device-expired-messages
Make "hide_device_expired_messages" more robust
2020-04-03 11:48:09 +02:00
Alexander Krotov
9c2a3b8a82 Chatlist::try_load: make hide_device_expired_messages errors non-fatal 2020-04-03 12:13:42 +03:00
Alexander Krotov
916fab7d4b hide_device_expired_messages: allow missing self or device chat 2020-04-03 12:12:56 +03:00
Alexander Krotov
3163ef87c6 imap: display errors with {}, not {:?} 2020-04-01 23:27:15 +03:00
Alexander Krotov
1934181b52 Terminate new SQL statement lines with \
This way rustfmt can change indentation. However, care should be taken
to keep a space before each \
2020-04-01 20:06:27 +03:00
Alexander Krotov
1b815a7d96 Display an error if message cannot be trashed 2020-04-01 20:06:27 +03:00
Alexander Krotov
4e0a08106d ChatId.get_param: remove unnecessary type annotation 2020-04-01 20:06:27 +03:00
Alexander Krotov
e8cc739fbd Reload chatlist when "delete_device_after" is set 2020-04-01 20:06:27 +03:00
Alexander Krotov
fc57cbfb49 Refactor hiding of expired messages
Now there is only one function: hide_device_expired_messages().

If any messages are hidden event DC_EVENT_MSGS_CHANGED(0,0) is emitted
now, which is more correct than DC_EVENT_CHAT_MODIFIED and also triggers
chatlist reload.
2020-04-01 20:06:27 +03:00
Alexander Krotov
7522fec044 Do not emit "chat modified" events when loading chatlist
Otherwise chatlist will be loaded twice, once right after hiding expired
messages, and once in response to event emitted by try_load().
2020-04-01 20:06:27 +03:00
Alexander Krotov
237dabb907 Do not hide hidden messages in ChatId.delete_device_expired_messages()
This prevents infinite event loop, when chatlist is reloaded in response
to event, and event is emitted by hiding messages before chatlist reload.
2020-04-01 20:06:27 +03:00
Alexander Krotov
3686048ab6 chatlist: hide all expired messages before loading 2020-04-01 20:06:27 +03:00
Alexander Krotov
2bf4c5d7e7 Emit "chat modified" event when messages expire 2020-04-01 20:06:27 +03:00
Alexander Krotov
051d80b2f3 Move delete_device_expired_messages() to ChatId
This allows to check if chat is a self-talk or device chat.

Messages are deleted only in viewed chats now.
2020-04-01 20:06:27 +03:00
Alexander Krotov
adaa1e856c chat: add ChatId.is_self_talk() and ChatId.is_device_talk() 2020-04-01 20:06:27 +03:00
Alexander Krotov
4daa57c98e delete_device_expired_messages: set text to DELETED for hidden messages
This makes debugging easier: DELETED messages should never be shown.
2020-04-01 20:06:27 +03:00
Alexander Krotov
7e67b2cbb3 Do not delete messages from special chats 2020-04-01 20:06:27 +03:00
Alexander Krotov
270d18a88a Move prune_tombstones() to sql:: and call from housekeeping() 2020-04-01 20:06:27 +03:00
Alexander Krotov
25f8a735a9 get_chat_msgs: remove locally expired messages
Expired messages are hidden right before retrieving messages from the
database, so expired messages are not shown to the user.
2020-04-01 20:06:27 +03:00
Alexander Krotov
9eb672ea17 Move removal of chat message tombstones to a separate function
DELETE operation may be slow compared to UPDATE. It is better to do in
a separate job.
2020-04-01 20:06:27 +03:00
Alexander Krotov
9febc762da Add chat::delete_device_expired_messages() function 2020-04-01 20:06:27 +03:00
Alexander Krotov
4b742c220c Add Context.get_config_delete_device_after() method
In contrast to get_config_delete_server_after(), value 1 does not mean
"delete at once", because it does not make sense to delete messages
immediately after receivning them.
2020-04-01 20:06:27 +03:00
Alexander Krotov
9d03d441e1 Add Config::DeleteDeviceAfter option 2020-04-01 20:06:27 +03:00
B. Petersen
ff8b249cc6 For now, 'Saved messages' are auto-deleted from the server, but not from device 2020-04-01 20:06:27 +03:00
B. Petersen
248e6ea5e7 make clippy happy 2020-04-01 20:06:27 +03:00
B. Petersen
be0afdebfd target comments of @link2xt, fix ci 2020-04-01 20:06:27 +03:00
B. Petersen
9f19d20344 implement message estimating 2020-04-01 20:06:27 +03:00
B. Petersen
aea8a32ba5 add 'estimatedeletion' to repl tool 2020-04-01 20:06:27 +03:00
B. Petersen
d1a4c82937 prototype ffi for ephemeral messages 2020-04-01 20:06:27 +03:00
Alexander Krotov
4f73812673 Prioritize message deletion over message moving
If Delta Chat goes online and gets an expired message is in the Inbox,
it should delete it instead of moving and then deleting.
2020-04-01 20:06:27 +03:00
Alexander Krotov
33150615a1 Improve logging for server UID updates in precheck_imf()
Log all folders and UIDs and warn about UID changes without folder change.
2020-04-01 20:06:27 +03:00
Alexander Krotov
491f83c86d Remove ServerFolder and ServerUid job parameters
They were used by MarkseenMdnOnImap
2020-04-01 20:06:27 +03:00
Alexander Krotov
41f776763b Remove MarkseenMdnOnImap
MarkseenMdnOnImap stored server folder and UID which are never updated
by update_server_uid. Now hidden entries are created for MDNs, so they
should be handled as ordinary messages.
2020-04-01 20:06:27 +03:00
Alexander Krotov
65fdfac866 Remove unused dest_uid argument from Imap.mv()
It was always set to 0 because we don't know the destination UID.
2020-04-01 20:06:27 +03:00
Alexander Krotov
cb0c00bc6d Log rfc724_mid in DeleteMsgOnImap 2020-04-01 20:06:27 +03:00
Alexander Krotov
ad53678c19 Remove msgs.unlinked column
It is not used anymore.

Database version 64 migration introducing this column is also removed.
2020-04-01 20:06:27 +03:00
Alexander Krotov
62097765a6 update_server_uid: set server_uid even for unlinked messages
Sometimes message deletion job marks message as unlinked without
actually deleting it. It is possible if the message was already moved
into another folder, possibly by second device, but not detected there
yet. It should be detected later in the other folder, and the
server_uid in the database should be set.

Since introduction of add_imap_deletion_jobs() any expired message
record will be marked as unlinked eventually, because another message
deletion job will be scheduled even if update_server_uid() resurrects
the message once.
2020-04-01 20:06:27 +03:00
Alexander Krotov
efb7280e99 Do not schedule delayed DeleteMsgOnImap jobs
All IMAP deletion jobs are scheduled either immediately, or later by
job::add_imap_deletion_jobs().
2020-04-01 20:06:27 +03:00
Alexander Krotov
bdb2a47743 Revert "Automatically delete messages in 2 weeks window only"
This may result in messages not being deleted. Optimization and
traffic-saving is postponed for later, one idea is to optimize message
deletion to avoid checking if Message-ID on the server matches
Message-ID in the database.
2020-04-01 20:06:27 +03:00
Alexander Krotov
c4677190be Postpone DeleteMsgOnImap on error
If job returns Status::Finished, it will be deleted. Then
add_imap_deletion_jobs will recreate it immediately if the message is
expired. To actually backoff the job, we should postpone it instead of
removing.
2020-04-01 20:06:27 +03:00
Alexander Krotov
055aba189c Automatically delete messages in 2 weeks window only
This is to avoid creating thousands of jobs when user enables
"delete_server_after" setting for the first time.

If device is offline for more than 2 weeks, some messages may not be
deleted.
2020-04-01 20:06:27 +03:00
Alexander Krotov
314c3d5e78 add_imap_deletion_jobs: check only for DeleteMsgOnImap jobs
Other jobs may have different meaning for foreign_id.

Also removed " \" at the end of lines.
2020-04-01 20:06:27 +03:00
Alexander Krotov
6db03356b5 Return AlreadyDone from Imap.delete_msg if message is gone
This way DeleteMsgOnImap will remove invalid server_uid from the database.
2020-04-01 20:06:27 +03:00
Alexander Krotov
28af919b09 Create DeleteMsgOnImap jobs before performing IMAP jobs
When "delete_server_after" setting is configured, postponed
DeleteMsgOnImap jobs are created for incoming messages.

This commit adds job::add_imap_deletion_jobs function which creates
DeleteMsgOnImap jobs right before performing IMAP jobs. This way even
messages that expired when the setting was disabled are going to be
deleted.

Job creation on message reception is unnecessary now, and even harmful
because it will create jobs with an expiration time which may later be
reduced. It is planned to be removed in following commits.
2020-04-01 20:06:27 +03:00
Alexander Krotov
8f7a456a39 Use 0 value for "delete_server_after" default.
Now 0 means "never delete", 1 means "delete at once" and other values
indicate the number of seconds after which them message should be
deleted from the server.

Configuration value interpretation is moved into
Context.get_config_delete_server_after() function.
2020-04-01 20:06:27 +03:00
Alexander Krotov
5b3bec1aac Create entries in msgs table for MDNs
At least one entry is required for DeleteMsgOnImap job. Additionally,
adding a hidden entry makes it possible to avoid redownloading the
message if it gets a new UID on the server.
2020-04-01 20:06:27 +03:00
Alexander Krotov
f2aa17c9d0 Resultify MsgId.delete_from_db() 2020-04-01 20:06:27 +03:00
Alexander Krotov
bc06b9e051 Do not send BCC-Self copy if we are going to remove it immediately 2020-04-01 20:06:27 +03:00
Alexander Krotov
6d216af507 Delete BCC-self messages after "delete_server_after" time 2020-04-01 20:06:27 +03:00
Alexander Krotov
b2f1d9f376 Do not remove rfc724_mid for unlinked messages
Message-ID is used to send read receipts. Instead, add a separate
"unlinked" column.
2020-04-01 20:06:27 +03:00
Alexander Krotov
a653e469f2 Add user-configurable option "delete_server_after"
The option sets timer in seconds after which all parts of the message
are deleted from the server.
2020-04-01 20:06:27 +03:00
Alexander Krotov
4f4241ba3a dc_receive_imf: delete all message parts if message should be deleted
DeleteMsgOnImap deletes files from the server only when the last part
is deleted. Removing only the first part of the hidden or trashed
message does not result in message deletion.
2020-04-01 20:06:27 +03:00
Alexander Krotov
2cf9c68040 Implement MsgId.unlink() and use it in DeleteMsgOnImap
Currently only trashed or hidden messages are deleted by
DeleteMsgOnImap, so it is safe to remove database records.

It is planned to delete messages on IMAP server after
user-configurable time to cleanup the server even for messages
displayed in chats. For such messages, we unlink them from the
Message-ID, but keep the database record to display them.
2020-04-01 20:06:27 +03:00
Alexander Krotov
cc0f977d6f Document rfc724_mid_cnt 2020-04-01 20:06:27 +03:00
Alexander Krotov
7879952fde Delete MDNs first in MsgId.delete_from_db() 2020-04-01 20:06:27 +03:00
Alexander Krotov
4452cab987 Turn Message::Delete_from_db into MsgId method 2020-04-01 20:06:27 +03:00
Alexander Krotov
98bd64621a Refactor Imap.delete_msg() error handling
Mutable UID reference is removed. Instead, ImapActionResult is
returned immediately.

If message has changed and UID does not correspond to expected
message, ImapActionResult::Failed is returned. Temprorary IMAP errors
result in ImapActionResult::RetryLater.

On the job side, ImapActionResult::RetryLater does not result in
immediate job retry anymore. Instead, job is retried with a backoff.
2020-04-01 20:06:27 +03:00
Alexander Krotov
c1c769ceb0 Add MsgId.trash() and use it to delete messages locally
In addition to moving the message into trash chat, this function
removes message text to make sure the message does not remain in the
database. Only the information necessary to delete message from the
server and avoid redownloading it should be kept, such as Message-Id
and IMAP UID.
2020-04-01 20:06:27 +03:00
Alexander Krotov
d64e55c66f delete_msgs: remove explicit .iter() 2020-04-01 20:06:27 +03:00
Alexander Krotov
76fc84be37 job: document DeleteMsgOnImap 2020-04-01 20:06:27 +03:00
bjoern
8cd5f5990e Merge pull request #1295 from deltachat/draft_group_consistency
draft doc for healing group-inconsistencies
2020-03-31 18:27:57 +02:00
B. Petersen
6ffe54d68f do no longer ignore keypair generation test, due to the ecc-move, it is no longer expensive 2020-03-31 18:20:40 +02:00
Alexander Krotov
d78ea882c8 Update mailparse to 0.12 2020-03-31 18:11:44 +02:00
Alexander Krotov
958802a233 Add system message only if contact was removed successfully 2020-03-31 18:08:19 +02:00
bjoern
00b02efdc2 Merge pull request #1367 from deltachat/no-forward-to-device-chat
hide the device-chat from default forward-to-lists
2020-03-31 16:03:53 +02:00
Floris Bruynooghe
50569f12f5 Remove unsafe CString::yolo from ffi
CString::yolo was still used in the ffi, this was an unsafe
transitional thing.  To remove it there were two choices: 1. make
errors in creating CStrings hard errors or 2. try and be as lenient as
possible.  Given the to_string_lossy() convention adopted in the ffi
this choose the lenient option and simply skips over embedded null
bytes, leaving the rest of the strings intact.

Thus now CString::new_lossy().  It's only used for .strdup() however
so no longer a public trait.

This also cleans up the public visibility of things in the strings.rs
file:

- Rename StrExt/OptStrExt traits to what they actually do: provide
  .strdup() -> Strdup/OptStrdup.

- dc_strdup() should be an implementation detail, replace all usages
  with Strdup.strdup() method.

- Only allow visibility inside the crate for all things.

- Reduce visibility to only the module for things not used in lib.rs.
2020-03-31 09:12:41 +02:00
B. Petersen
8aa4ceb570 hide the device-chat from default forward-to-lists 2020-03-30 15:59:07 +02:00
Alexander Krotov
a7afbf85ad Replace test Alice key with ed25519 key
Autocrypt Setup Message does not contain "==" anymore since key length
has changed.
2020-03-29 19:52:45 +03:00
bjoern
8fdf3dcdb8 Merge pull request #1314 from deltachat/fix-secure-join
ignore handshake messages seen from another device
2020-03-28 10:28:55 +01:00
B. Petersen
818c20e0cb switch to ecc keys
after fixing some issues wrt ecc keys, see #1319,
and waiting some time (three core releases, two ios/android/desktop releases),
it is now the time to switch again to ecc keys again,
after the first attempt was stopped in #1319
2020-03-28 01:05:24 +03:00
bjoern
c1d4996777 Merge pull request #1361 from deltachat/prep-1.28
Prep 1.28
2020-03-25 22:09:34 +01:00
B. Petersen
fd3e6e0ee4 bump version to 1.28 2020-03-25 19:30:52 +01:00
B. Petersen
edc5754c68 changelog 2020-03-25 19:29:44 +01:00
bjoern
bd75dea000 Merge pull request #1359 from deltachat/fix-imap-delimiter
fix imap delimiter
2020-03-25 19:01:58 +01:00
B. Petersen
e09a0a548f using first() instead of [0] 2020-03-25 18:10:02 +01:00
bjoern
15ee8b4362 Merge pull request #1357 from deltachat/ad-hoc-groups
fix classic-email interactions
2020-03-25 17:57:59 +01:00
B. Petersen
ab8d75b192 remove now unused ImapConfig.imap_delimiter 2020-03-25 13:23:29 +01:00
B. Petersen
e135c969c9 figure out and use folder-delimiter provided by LIST command 2020-03-25 13:16:23 +01:00
B. Petersen
36e7090466 force folder-reconfigure on dc_configure() 2020-03-25 02:39:48 +01:00
B. Petersen
f28f177c6b use constant for folders_configured cache 2020-03-24 19:18:18 +01:00
B. Petersen
785973c624 allow ad-hoc-group creation if there is an accepted chat with the contact 2020-03-24 16:47:54 +01:00
B. Petersen
9c06acff72 add tests wrt classic email-interaction 2020-03-24 16:47:54 +01:00
bjoern
4fabddeb47 Merge pull request #1354 from deltachat/fix-bcc-self-group-crash
fix crash in self-only-groups with bcc_self enabled
2020-03-23 12:21:27 +01:00
B. Petersen
17ff1ab372 this corrects the To:-list for group-messages with only SELF as member.
before, the list was empty
and trying to send to groups that only contain SELF lead to a crash.

in theory, this happens for both, bcc_self enabled or not, however,
if bcc_self was disabled (default setting),
things worked as the whole mimerendering was skipped on a higher level.
also, the saved-messages-chat was not affected as this was checked explicitly.

this pr changes the mimerendering so that From: is used as To:
if there is no recipient-list and the messasge will be sent to SELF only.
2020-03-23 01:48:27 +01:00
B. Petersen
3c34096392 add a failing test that crashes when sending a message to a self-only group with bcc_self enabled 2020-03-22 18:23:55 +01:00
bjoern
70e0d3b571 Merge pull request #1353 from deltachat/show-nondc-replies
fix showing non-dc replies
2020-03-22 16:48:49 +01:00
B. Petersen
ae5a2396f3 fix typo 2020-03-22 16:18:53 +01:00
B. Petersen
8f82bf40e0 let function that search for Message-IDs accept widly used square brackets format 2020-03-22 12:20:28 +01:00
B. Petersen
fe398de2fa add test that fail on checking existance of a Message-ID with angle brackets 2020-03-22 12:20:28 +01:00
Alexander Krotov
a770d75e2e Fix condition in normalize_name() 2020-03-22 00:11:41 +00:00
Alexander Krotov
a330104e9b Add tests that crash normalize_name() 2020-03-22 00:11:41 +00:00
bjoern
aae3cae4bb Merge pull request #1342 from deltachat/group-rejoin-bug
Rebuild group member list on group rejoin
2020-03-20 00:39:26 +01:00
Alexander Krotov
e7e4821804 Reset group member list when we are added to the group
This makes test_synchronize_member_list_on_group_rejoin pass.
2020-03-19 17:03:31 +03:00
Alexander Krotov
9654802acc Add failing group rejoin test 2020-03-19 17:03:31 +03:00
Friedel Ziegelmayer
06a24fa4d0 Merge pull request #1345 from deltachat/ci/fix-online-tests
ci: ensure dcc_new_temp_email is propagated
2020-03-19 15:02:30 +01:00
dignifiedquire
62b1b0519a ci: ensure dcc_new_temp_email is propagated
Closes #1344
2020-03-19 11:14:37 +01:00
bjoern
10afdfecdd Merge pull request #1336 from deltachat/forward-to-saved
optionally sort 'saved messages' atop of the chatlist
2020-03-11 22:05:50 +01:00
B. Petersen
c0e08fb927 fix typo 2020-03-11 16:17:29 +01:00
B. Petersen
6d6bc9b050 for forwarding, sort 'saved messages' atop of the chatlist 2020-03-09 23:30:22 +01:00
Alexander Krotov
4714fb6887 Reset server_folder and server_uid in Imap.empty_folder()
This way we avoid trying to delete already deleted messages in the future.
2020-03-09 01:21:12 +03:00
bjoern
5f47810964 Merge pull request #1328 from deltachat/prep-1.27
Prep 1.27
2020-03-04 18:37:31 +01:00
B. Petersen
7c39bb6659 ignore handshake messages seen from another device
moreover, prepare for marking peers as verified accross devices,
see detailed comment for observe_securejoin_on_other_device()
2020-02-27 15:04:11 +01:00
holger krekel
13e361aabc add two variants 2020-02-19 14:21:15 +01:00
holger krekel
d1a26e66a7 update 2020-02-19 13:48:05 +01:00
holger krekel
ffe3c84e7c small refinements 2020-02-19 13:39:33 +01:00
holger krekel
702c7382a7 move 2020-02-19 13:35:07 +01:00
holger krekel
b138d486e4 two ideas to tackle group consistency 2020-02-19 13:29:58 +01:00
42 changed files with 2425 additions and 370 deletions

View File

@@ -1,5 +1,19 @@
# Changelog
## 1.28.0
- new flag DC_GCL_FOR_FORWARDING for dc_get_chatlist()
that will sort the "saved messages" chat to the top of the chatlist #1336
- mark mails as being deleted from server in dc_empty_server() #1333
- fix interaction with servers that do not allow folder creation on root-level;
use path separator as defined by the email server #1359
- fix group creation if group was created by non-delta clients #1357
- fix showing replies from non-delta clients #1353
- fix member list on rejoining left groups #1343
- fix crash when using empty groups #1354
- fix potential crash on special names #1350
## 1.27.0
- handle keys reliably on armv7 #1327

58
Cargo.lock generated
View File

@@ -373,6 +373,24 @@ name = "bytes"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bzip2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bzip2-sys 0.1.8+1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bzip2-sys"
version = "0.1.8+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "c2-chacha"
version = "0.2.3"
@@ -624,7 +642,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.27.0"
version = "1.28.0"
dependencies = [
"async-imap 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"async-native-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -651,12 +669,12 @@ dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lettre_email 0.9.2 (git+https://github.com/deltachat/lettre)",
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
"mailparse 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)",
"mailparse 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"num-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"pgp 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"pgp 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"pretty_env_logger 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"proptest 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -678,6 +696,7 @@ dependencies = [
"strum_macros 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"thread-local-object 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"zip 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -690,9 +709,9 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.27.0"
version = "1.28.0"
dependencies = [
"deltachat 1.27.0",
"deltachat 1.28.0",
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"human-panic 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1473,7 +1492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "mailparse"
version = "0.10.4"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1814,7 +1833,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "pgp"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1903,6 +1922,11 @@ dependencies = [
"inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "podio"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ppv-lite86"
version = "0.2.6"
@@ -3136,6 +3160,18 @@ dependencies = [
"synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zip"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bzip2 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
"podio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
"checksum aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "54eb1d8fe354e5fc611daf4f2ea97dd45a765f4f1e4512306ec183ae2e8f20c9"
@@ -3180,6 +3216,8 @@ dependencies = [
"checksum bytecount 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b92204551573580e078dc80017f36a213eb77a0450e4ddd8cfa0f3f2d1f0178f"
"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
"checksum bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1"
"checksum bzip2 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b"
"checksum bzip2-sys 0.1.8+1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "05305b41c5034ff0e93937ac64133d109b5a2660114ec45e9760bc6816d83038"
"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
"checksum cargo_metadata 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e5d1b4d380e1bab994591a24c2bdd1b054f64b60bef483a8c598c7c345bc3bbe"
"checksum cast5 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ce5759b4c52ca74f9a98421817c882f1fd9b0071ae41cd61ab9f9d059c04fd6"
@@ -3299,7 +3337,7 @@ dependencies = [
"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
"checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
"checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084"
"checksum mailparse 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6c03df7fe4bab038aaa2f313baae7600de0afd606f8244860801c46f53babdd8"
"checksum mailparse 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7181507a68fef921f011b0c0f143efca20871da5ab3963bdc064537278469cd2"
"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
"checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
"checksum md-5 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a18af3dcaf2b0219366cdb4e2af65a6101457b415c3d1a5c71dd9c2b7c77b9c8"
@@ -3336,13 +3374,14 @@ dependencies = [
"checksum parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "92e98c49ab0b7ce5b222f2cc9193fc4efe11c6d0bd4f648e374684a6857b1cfc"
"checksum parking_lot_core 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7582838484df45743c8434fbff785e8edf260c28748353d44bc0da32e0ceabf1"
"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
"checksum pgp 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "96c659fb6141cb4b6bd2c50af03869b82789291770f0b035f36ab92eba5d8663"
"checksum pgp 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8172973101790c866e66966002bf1028d0df27bf6b3b29be86a6fd440d8a4285"
"checksum pin-project 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7804a463a8d9572f13453c516a5faea534a2403d7ced2f0c7e100eeff072772c"
"checksum pin-project-internal 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "385322a45f2ecf3410c68d2a549a4a2685e8051d0f278e39743ff4e451cb9b3f"
"checksum pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae"
"checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587"
"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
"checksum png 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef859a23054bbfee7811284275ae522f0434a3c8e7f4b74bd4a35ae7e1c4a283"
"checksum podio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "780fb4b6698bbf9cf2444ea5d22411cef2953f0824b98f33cf454ec5615645bd"
"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
"checksum pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
"checksum pretty_env_logger 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "717ee476b1690853d222af4634056d830b5197ffd747726a9a1eee6da9f49074"
@@ -3485,3 +3524,4 @@ dependencies = [
"checksum x25519-dalek 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217"
"checksum zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8"
"checksum zeroize_derive 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2"
"checksum zip 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6df134e83b8f0f8153a094c7b0fd79dfebe437f1d76e7715afa18ed95ebe2fd7"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.27.0"
version = "1.28.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -53,11 +53,12 @@ bitflags = "1.1.0"
debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.10.2"
mailparse = "0.12.0"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
pretty_env_logger = "0.3.1"
zip = "0.5"
rustyline = { version = "4.1.0", optional = true }

788
assets/exported-chat.css Normal file
View File

@@ -0,0 +1,788 @@
/* TODO inlcude the referenced svgs as base64 data uris */
.header {
background-color: #415e6b;
color: #fff;
position: absolute;
height: 52px;
width: 100%;
z-index: 5;
display:flex;
}
.header .avatar {
height: 36px;
width: 36px;
border-radius: 100%;
user-select: none;
margin: 8px;
}
.header .avatar.text-avatar {
background-color: #505050;
color: white;
font-size: 26px;
line-height: 36px;
text-align: center;
}
.header .name {
height: 52px;
line-height: 52px;
margin-left: 3px;
font-size: 20px;
}
.message.outgoing .author-avatar, .message.outgoing .author {
display: none!important;
}
:root {--colorPrimary: #42A5F5;--colorDanger: #f96856;--colorNone: #a0a0a0;--ovalButtonBg: #415e6b;--ovalButtonBgHover: rgb(120, 156, 173);--ovalButtonText: #fff;--ovalButtonTextHover: rgb(0, 0, 0);--navBarBackground: #415e6b;--navBarText: #fff;--navBarSearchPlaceholder: rgb(186, 186, 186);--navBarGroupSubtitle: rgb(186, 186, 186);--chatViewBg: #e6dcd4;--chatViewBgImgPath: url(../images/background_light.svg);--composerBg: #fff;--composerText: #010101;--composerPlaceholderText: rgba(1, 1, 1, 0.5);--composerBtnColor: rgba(1, 1, 1, 0.9);--composerSendButton: #415e6b;--emojiSelectorSelectionColor: #2090ea;--chatListItemSelectedBg: #4c6e7d;--chatListItemSelectedBgHover: #5E889B;--chatListItemSelectedText: #fff;--chatListItemBgHover: rgb(228, 228, 228);--chatListBorderColor: #b9b9b9;--chatListBorder: 1px solid undefined;--messageText: #010101;--messageTextLink: #010101;--setupMessageText: #ed824e;--infoMessageBubbleBg: #0000008c;--infoMessageBubbleText: white;--messageIncommingBg: #fff;--messageIncommingDate: #010101;--messageOutgoingBg: #efffde;--messageOutgoingStatusColor: #4caf50;--messageButtons: #8b8e91;--messageButtonsHover: #070c14;--messageStatusIcon: #4caf50;--messageStatusIconSending: #62656a;--messagePadlockOutgoing: #4caf50;--messagePadlockIncomming: #a4a6a9;--messageMetadataDate: #62656a;--messageMetadataIncomming: rgba(#ffffff, 0.7);--messageAuthor: #ffffff;--messageAttachmentIconExtentionColor: #070c14;--messageAttachmentIconBg: transparent;--messageAttachmentFileInfo: #010101;--loginInputFocusColor: #42A5F5;--loginButtonText: #42A5F5;--deltaChatPrimaryFg: #010101;--deltaChatPrimaryFgLight: #62656a;--contextMenuBg: #fff;--contextMenuBorder: rgb(221, 221, 221);--contextMenuText: #62656a;--contextMenuSelected: #f5f5f5;--contextMenuSelectedBg: #a4a6a9;--bp3DialogHeaderBg: #fff;--bp3DialogHeaderIcon: #666666;--bp3DialogBgSecondary: #ececec;--bp3DialogBgPrimary: #fff;--bp3Heading: #010101;--bp3ButtonText: #010101;--bp3ButtonBg: #fff;--bp3ButtonGradientTop: rgba(255,255,255,0.8);--bp3ButtonGradientBottom: rgba(255,255,255,0);--bp3ButtonHoverBg: #ebf1f5;--bp3InputText: #010101;--bp3InputBg: #fff;--bp3InputPlaceholder: lightgray;--bp3MenuText: #010101;--bp3MenuBg: #fff;--bp3Switch: #7a8084;--bp3SwitchShadow: unset;--bp3SwitchChecked: #acd4e8;--bp3SwitchShadowChecked: unset;--bp3SwitchKnob: #f5f5f5;--bp3SwitchKnobShadow: 0px 2px 0 0px #d2cfcfad;--bp3SwitchKnobChecked: #42A5F5;--bp3SwitchKnobShadowChecked: 0px 1px 0 0px #c9d4d2d1;--bp3SpinnerTrack: #acd4e8;--bp3SpinnerHead: #42a5f5;--bp3SelectorTop: rgba(255, 255, 255, 0.8);--bp3SelectorBottom: rgba(255, 255, 255, 0.0);--outlineProperties: 1px solid transparent;--outlineColor: b9b9b9;--emojiMartText: #010101;--emojiMartSearchBorder: lightgrey;--emojiMartBg: #fff;--emojiMartOutsideRadius: 5px;--emojiMartCategoryIcons: rgb(99, 99, 99);--emojiMartInputBg: #f5f5f5;--emojiMartInputText: #010101;--emojiMartInputPlaceholder: rgb(74, 74, 74);--emojiMartSelect: rgb(198, 198, 198);--galleryBg: #fff;--avatarLabelColor: #ffffff;--brokenMediaText: #070c14;--brokenMediaBg: #ffffff;--unreadCountBg: #2090ea;--unreadCountLabel: #ffffff;--contactListItemBg: #62656a;--contactListInitalColor: #62656a;--contactEmailColor: #62656a;--errorColor: #f44336;--globalLinkColor: #2090ea;--globalBackground: #fff;--globalText: #010101;--mapOverlayBg: #fff;--videoPlayBtnIcon: #2090ea;--videoPlayBtnBg: #ffffff;--scrollbarThumb: #666666;--scrollbarThumbHover: #606060;}
* {
box-sizing: border-box;
}
html {
height: 100%;
--messageIncommingBg: rgb(232, 232, 232);
}
body {
position: relative;
height: 100%;
width: 100%;
margin: 0;
font-family: Roboto, "Apple Color Emoji", NotoEmoji, "Helvetica Neue", Arial,
Helvetica, NotoMono, sans-serif !important;
font-size: 14px;
color: black;
background-color: white;
}
ul {
list-style: none;
padding-left: 0;
}
input:focus {
outline: 0 !important;
}
button:focus {
outline: none;
}
button:focus {
outline: none;
}
::-webkit-scrollbar {
width: 6px;
height: 0;
}
::-webkit-scrollbar-track {
background: white;
}
::-webkit-scrollbar-thumb {
background: var(--scrollbarThumb);
}
::-webkit-scrollbar-thumb:hover {
background: var(--scrollbarThumbHover);
}
::-webkit-scrollbar-corner {
background: transparent;
}
span.module-contact-name {
font-weight: 200;
font-size: medium;
}
.module-contact-name__profile-name {
font-style: italic;
}
.AvatarBubble {
position: relative;
z-index: 2;
object-fit: cover;
height: 48px;
width: 48px;
margin-top: 8px;
margin-bottom: 8px;
border-radius: 100%;
background-color: #505050;
color: var(--avatarLabelColor);
font-size: 26px;
line-height: 48px;
text-align: center;
user-select: none;
}
.AvatarBubble.large {
height: 64px;
width: 64px;
line-height: 64px;
font-size: 39px;
}
.AvatarBubble--NoSearchResults {
transform: rotate(45deg);
line-height: 46px;
letter-spacing: 1px;
}
.AvatarBubble--NoSearchResults::after {
content: ":-(";
}
.AvatarImage {
position: relative;
z-index: 2;
object-fit: cover;
height: 48px;
width: 48px;
margin-top: 8px;
margin-bottom: 8px;
border-radius: 100%;
user-select: none;
}
.AvatarImage.large {
height: 64px;
width: 64px;
}
.attachment-overlay .attachment-view {
height: 100%;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: #313131;
}
.attachment-overlay .attachment-view img,
.attachment-overlay .attachment-view video {
width: 100vw;
max-height: 100vh;
object-fit: contain;
}
.attachment-overlay .attachment-view video {
width: 95vw;
}
.attachment-overlay .render-media-wrapper {
width: 100vw;
height: 100vh;
}
.attachment-overlay .btn-wrapper {
float: right;
position: absolute;
z-index: 10;
cursor: pointer;
}
.attachment-overlay .download-btn {
height: 36px;
width: 36px;
display: inline-block;
-webkit-mask: url("../images/download.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageButtons);
}
.attachment-overlay .download-btn:hover {
background-color: var(--messageButtons);
}
.message-attachment-media {
text-align: center;
position: relative;
cursor: pointer;
margin-left: -12px;
margin-right: -12px;
margin-top: -10px;
margin-bottom: -10px;
border-radius: 16px;
overflow: hidden;
background-color: var(--messageAttachmentIconBg);
}
.message-attachment-media > .attachment-content {
object-fit: scale-down;
object-position: center;
min-height: 150px;
max-height: 300px;
max-width: 40vw;
margin-bottom: -4px;
cursor: pointer;
}
.message-attachment-media > .video-play-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 48px;
height: 48px;
background-color: var(--videoPlayBtnBg);
border-radius: 24px;
}
.message-attachment-media > .video-play-btn > .video-play-btn-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 36px;
width: 36px;
-webkit-mask: url("../images/play.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--videoPlayBtnIcon);
}
.message-attachment-media.content-below {
margin-bottom: 7px;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.message-attachment-media.content-above {
margin-top: 4px;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
.message-attachment-broken-media {
font-size: 11px;
line-height: 16px;
letter-spacing: 0.3px;
padding: 10px;
text-align: center;
text-transform: uppercase;
color: var(--brokenMediaBg);
}
.message-attachment-broken-media.incoming {
color: var(--brokenMediaText);
}
.message-attachment-audio {
margin-top: 2px;
display: block;
margin-right: 30px;
}
.message-attachment-audio.content-below {
margin-bottom: 5px;
}
.message-attachment-audio.content-above {
margin-top: 6px;
}
.message-attachment-generic {
display: flex;
flex-direction: row;
}
.message-attachment-generic.content-below {
padding-bottom: 6px;
}
.message-attachment-generic.content-above {
padding-top: 4px;
}
.message-attachment-generic > .file-icon {
background: url("../images/file-gradient.svg") no-repeat center;
height: 44px;
width: 56px;
margin-left: -13px;
margin-right: -14px;
margin-bottom: -4px;
display: flex;
flex-direction: row;
align-items: center;
}
.message-attachment-generic > .file-icon > .file-extension {
font-size: 10px;
line-height: 13px;
letter-spacing: 0.1px;
text-transform: uppercase;
text-align: center;
width: 25px;
margin-left: auto;
margin-right: auto;
overflow-x: hidden;
white-space: nowrap;
text-overflow: clip;
color: var(--messageAttachmentIconExtentionColor);
font-family: monospace;
}
.message-attachment-generic > .text-part {
flex-grow: 1;
margin-left: 8px;
max-width: calc(100% - 37px);
}
.message-attachment-generic > .text-part > .name {
color: var(--messageAttachmentFileInfo);
font-size: 14px;
line-height: 18px;
font-weight: 300;
margin-top: 2px;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.message-attachment-generic > .text-part > .size {
color: var(--messageAttachmentFileInfo);
font-size: 11px;
line-height: 16px;
letter-spacing: 0.3px;
margin-top: 3px;
}
.module-message-detail {
margin-top: -20px;
}
.module-message-detail .bp3-callout {
max-height: 50vh;
overflow: auto;
}
.module-message-detail p {
white-space: pre-line;
user-select: text;
}
.module-message-detail__message-container {
padding-top: 20px;
padding-bottom: 20px;
}
.module-message-detail__message-container:after {
content: ".";
visibility: hidden;
display: block;
height: 0;
clear: both;
}
.module-message-detail__label {
font-weight: 300;
padding-right: 5px;
}
.module-message-detail__unix-timestamp {
color: #eeefef;
}
.module-message-detail__delete-button-container {
text-align: center;
margin-top: 10px;
}
.module-message-detail__delete-button {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
background-color: #f44336;
color: #fff;
box-shadow: 0 0 10px -3px rgba(97, 97, 97, 0.7);
border-radius: 5px;
border: solid 1px #a4a6a9;
cursor: pointer;
margin: 1em auto;
padding: 1em;
}
.module-message-detail .message-content * {
background-color: lightgrey;
width: 100%;
resize: none;
padding: 1rem;
}
.message-list-and-composer {
width: 70%;
float: right;
display: grid;
grid-template-columns: auto;
height: calc(100vh - 50px);
margin-top: 50px;
background-image: var(--chatViewBgImgPath);
background-size: cover;
background-color: var(--chatViewBg);
}
.message-list-and-composer__message-list #message-list {
background: #dbdbdb;
position: absolute;
bottom: 0;
overflow: scroll;
max-height: 100%;
width: 100%;
padding: 0 0.5em;
top: 52px;
}
.message-list-and-composer__message-list
#message-list::-webkit-scrollbar-track {
background: transparent;
}
.message-list-and-composer__message-list ul {
list-style: none;
min-width: 200px;
}
.message-list-and-composer__message-list ul li {
margin-bottom: 10px;
min-width: 200px;
}
.message-list-and-composer__message-list ul li::after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.message-list-and-composer__message-list ul li .info-message {
max-width: 550px;
font-size: 1rem;
padding: 2rem;
font-style: normal;
white-space: pre-wrap;
text-align: left;
}
.message {
position: relative;
display: inline-flex;
flex-direction: row;
align-items: stretch;
}
.message:hover .message-buttons {
opacity: 1;
}
.message > .author-avatar {
align-self: flex-end;
bottom: 0px;
position: static;
margin-right: 8px;
user-select: none;
}
.message > .author-avatar img {
height: 36px;
width: 36px;
border-radius: 18px;
object-fit: cover;
}
.message > .author-avatar.default {
text-align: center;
}
.message > .author-avatar.default > .label {
user-select: none;
color: var(--avatarLabelColor);
top: -121px;
left: -10px;
border-radius: 50%;
width: 36px;
height: 36px;
font-size: 25px;
line-height: 36px;
}
.message .message-buttons {
position: absolute;
top: 5px;
right: -4px;
display: inline-flex;
flex-direction: row;
align-items: center;
opacity: 0;
z-index: 10;
user-select: text;
}
.message .message-buttons .msg-button {
height: 24px;
width: 24px;
display: inline-block;
cursor: pointer;
}
.message .message-buttons .msg-button:hover {
background-color: var(--messageButtons);
}
.message .message-buttons .msg-button.download {
-webkit-mask: url("../images/download.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageButtons);
}
.message .message-buttons .msg-button.reply {
display: none;
-webkit-mask: url("../images/reply.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageButtons);
user-select: none;
}
.message .message-buttons .msg-button.menu {
-webkit-mask: url("../images/ellipsis.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageButtons);
transform: rotate(90deg);
-webkit-mask-position-y: 4px;
user-select: none;
}
.message .msg-container {
position: relative;
display: inline-block;
border-radius: 16px;
padding-right: 12px;
padding-left: 12px;
padding-top: 10px;
padding-bottom: 10px;
}
.message .msg-container > .author {
display: inline-block;
max-width: 40vw;
font-size: 13px;
font-weight: 300;
line-height: 18px;
height: 18px;
overflow-x: hidden;
overflow-y: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.message .msg-container .msg-body.msg-body--clickable {
cursor: pointer;
}
.message .msg-container .msg-body > .text {
color: var(--messageText);
font-size: 14px;
line-height: 18px;
text-align: start;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
white-space: pre-wrap;
margin-right: 10px;
}
.message .msg-container .msg-body > .text a {
text-decoration: underline;
color: var(--messageTextLink);
}
.message .msg-container .msg-body > .text .double-line-break {
height: 28px;
}
.message .msg-container .msg-body > .text .line-break {
height: 14px;
}
.message .msg-container .msg-body > .text .line-break:last-child {
height: 0px;
}
.message .metadata {
margin-top: 10px;
margin-bottom: -7px;
float: right;
}
.message .module-message__img-attachment {
object-fit: cover;
width: auto;
max-width: 100%;
min-height: unset;
}
.message.incoming {
margin-left: 0;
margin-right: 32px;
}
.message.incoming .metadata:not(.with-image-no-caption) > .padlock-icon {
-webkit-mask: url("../images/padlock.svg") no-repeat center;
-webkit-mask-size: 125%;
background-color: var(--messagePadlockIncomming);
}
.message.incoming .metadata:not(.with-image-no-caption) > .location-icon {
-webkit-mask: url("../images/map-marker.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messagePadlockIncomming);
}
.message.incoming .metadata:not(.with-image-no-caption) > .date {
color: var(--messageMetadataIncomming);
}
.message.incoming .msg-container {
background-color: var(--messageIncommingBg);
}
.message.incoming .msg-container,
.message.incoming .msg-container .message-attachment-media {
border-bottom-left-radius: 1px;
}
.message.outgoing {
float: right;
margin-right: 0;
margin-left: 32px;
}
.message.outgoing .metadata > .date {
color: var(--messageOutgoingStatusColor);
}
.message.outgoing .metadata > .padlock-icon {
-webkit-mask: url("../images/padlock.svg") no-repeat center;
-webkit-mask-size: 125%;
background-color: var(--messagePadlockOutgoing);
}
.message.outgoing .metadata > .location-icon {
-webkit-mask: url("../images/map-marker.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messagePadlockOutgoing);
}
.message.outgoing .metadata > .status-icon.read,
.message.outgoing .metadata > .status-icon.delivered {
background-color: var(--messageOutgoingStatusColor);
}
.message.outgoing .msg-container {
background-color: var(--messageOutgoingBg);
}
.message.outgoing .msg-container,
.message.outgoing .msg-container .message-attachment-media {
border-bottom-right-radius: 1px;
}
.message.type-sticker .msg-container {
background-color: transparent !important;
}
.message.type-sticker .message-attachment-media {
background-color: transparent;
}
.message.type-sticker .message-attachment-media > .attachment-content {
margin-bottom: 20px;
}
.message.type-sticker .metadata {
float: right;
padding: 4px 10px 1px 10px;
margin-bottom: -7px;
background-color: #01010159;
border-radius: 4px;
color: black;
font-weight: bold;
}
.message.type-sticker .metadata > .date {
font-size: 11px;
color: white;
}
.message.type-sticker .metadata > .padlock-icon {
-webkit-mask: url("../images/padlock.svg") no-repeat center;
-webkit-mask-size: 125%;
background-color: #fff;
}
.message.type-sticker .metadata > .location-icon {
-webkit-mask: url("../images/map-marker.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: #fff;
}
.message.type-sticker .status-icon.read,
.message.type-sticker .status-icon.delivered {
background-color: white;
}
.message.type-sticker:hover .msg-button.menu {
background-color: white;
}
.message.type-sticker:hover .react-contextmenu-wrapper {
background-color: #2525258f;
border-radius: 4px;
}
.message.error.incoming .text {
font-style: italic;
}
.message.forwarded .forwarded-indicator {
font-weight: bold;
font-size: 0.9em;
margin-bottom: 3px;
opacity: 0.86;
}
.message.forwarded .message-attachment-media {
border-top-left-radius: 0;
border-top-right-radius: 0;
margin-top: 0;
}
.setupMessage .message .text {
color: var(--setupMessageText);
}
.hide-on-small {
display: initial;
}
@media (max-width: 800px) {
.hide-on-small {
display: none;
}
}
@media (min-width: 800px) and (max-width: 925px) {
.message {
max-width: 374px;
}
.message.incoming {
margin-right: auto;
}
.message.outgoing {
margin-left: auto;
}
}
@media (min-width: 926px) {
.message {
max-width: 66%;
}
.message.incoming {
margin-right: auto;
}
.message.outgoing {
margin-left: auto;
}
}
.metadata {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 3px;
margin-bottom: -3px;
}
.metadata.with-image-no-caption {
position: absolute;
right: 5px;
bottom: 5px;
float: right;
padding: 4px 10px 1px 10px;
margin: 0;
background-color: #0000008f;
border-radius: 4px;
font-weight: bold;
}
.metadata.with-image-no-caption > .date {
color: white;
}
.metadata.with-image-no-caption > .padlock-icon {
-webkit-mask: url("../images/padlock.svg") no-repeat center;
-webkit-mask-size: 125%;
background-color: #fff;
}
.metadata.with-image-no-caption .status-icon.sending {
background-color: white;
}
.metadata > .status-icon {
margin-bottom: 2px;
}
.metadata > .username {
margin-right: 10px;
}
.metadata > .date {
font-size: 11.5px;
line-height: 16px;
letter-spacing: 0.3px;
color: var(--messageMetadataDate);
text-transform: uppercase;
}
.metadata > .spacer {
flex-grow: 1;
}
.metadata > .padlock-icon,
.metadata > .location-icon {
width: 12px;
height: 12px;
display: inline-block;
margin-right: 2px;
margin-bottom: 3px;
}
.metadata > .location-icon {
margin-bottom: 0;
}
@keyframes __status-icon--spinning {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.status-icon {
width: 18px;
height: 12px;
display: inline-block;
margin-left: 2px;
}
.status-icon.sending {
-webkit-mask: url("../images/sending.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageStatusIconSending);
animation: __status-icon--spinning 4s linear infinite;
width: 12px;
margin-left: 8px;
}
.status-icon.delivered {
-webkit-mask: url("../images/sent.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageStatusIcon);
}
.status-icon.read {
-webkit-mask: url("../images/read.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--messageStatusIcon);
}
.status-icon.error {
-webkit-mask: url("../images/error.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: var(--errorColor);
width: 12px;
margin-left: 8px;
}

View File

@@ -46,6 +46,7 @@ if [ -n "$TESTS" ]; then
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
unset DCC_PY_LIVECONFIG
unset DCC_NEW_TMP_EMAIL
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
tox --workdir "$TOXWORKDIR" -e auditwheels
popd

View File

@@ -32,11 +32,11 @@ ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
set +x -e
cd $BUILDDIR
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
set -x
# run everything else inside docker
docker run -e DCC_PY_LIVECONFIG \
docker run -e DCC_NEW_TMP_EMAIL -e DCC_PY_LIVECONFIG \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh

View File

@@ -30,6 +30,7 @@ ssh $SSHTARGET <<_HERE
export CARGO_TARGET_DIR=\`pwd\`/../target
export TARGET=release
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
#we rely on tox/virtualenv being available in the host
#rm -rf virtualenv venv

View File

@@ -37,7 +37,8 @@ mkdir -p $TOXWORKDIR
# XXX we may switch on some live-tests on for better ensurances
# Note that the independent remote_tests_python step does all kinds of
# live-testing already.
unset DCC_PY_LIVECONFIG
unset DCC_PY_LIVECONFIG
unset DCC_NEW_TMP_EMAIL
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
popd

View File

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

View File

@@ -373,6 +373,14 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
* >=1=seconds, after which messages are deleted automatically from the device.
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
* >=1=seconds, after which messages are deleted automatically from the server.
* "Saved messages" are deleted from the server as well as
* emails matching the `show_emails` settings above, the UI should clearly point that out.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -901,6 +909,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
#define DC_GCL_ARCHIVED_ONLY 0x01
#define DC_GCL_NO_SPECIALS 0x02
#define DC_GCL_ADD_ALLDONE_HINT 0x04
#define DC_GCL_FOR_FORWARDING 0x08
/**
@@ -939,6 +948,8 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
* if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
* the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
* chats
* - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist,
* typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
* - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
* to the list (may be used eg. for selecting chats on forwarding, the flag is
* not needed when DC_GCL_ARCHIVED_ONLY is already set)
@@ -1292,6 +1303,21 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
/**
* Estimate the number of messages that will be deleted
* by the dc_set_config()-options `delete_device_after` or `delete_server_after`.
* This is typically used to show the estimated impact to the user before actually enabling ephemeral messages.
*
* @param context The context object as returned from dc_context_new().
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
* @param seconds Count messages older than the given number of seconds.
* @return Number of messages that are older than the given number of seconds.
* This includes emails downloaded due to the `show_emails` option.
* Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
*/
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
/**
* Returns the message IDs of all _fresh_ messages of any chat.
* Typically used for implementing notification summaries.
@@ -1655,8 +1681,9 @@ char* dc_get_mime_headers (dc_context_t* context, uint32_t ms
*/
void dc_delete_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
/**
/*
* Empty IMAP server folder: delete all messages.
* Deprecated, use dc_set_config() with the key "delete_server_after" instead.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new()
@@ -4124,28 +4151,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
/**
* @defgroup DC_EMPTY DC_EMPTY
*
* These constants configure emptying imap folders with dc_empty_server()
*
* @addtogroup DC_EMPTY
* @{
*/
/**
* Clear all mvbox messages.
*/
#define DC_EMPTY_MVBOX 0x01
/**
* Clear all INBOX messages.
*/
#define DC_EMPTY_INBOX 0x02
/**
* @}
*/
#define DC_EMPTY_MVBOX 0x01 // Deprecated, flag for dc_empty_server(): Clear all mvbox messages
#define DC_EMPTY_INBOX 0x02 // Deprecated, flag for dc_empty_server(): Clear all INBOX messages
/**

View File

@@ -433,7 +433,7 @@ pub unsafe extern "C" fn dc_set_config_from_qr(
pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_info()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_context = &*context;
let guard = ffi_context.inner.read().unwrap();
@@ -1045,6 +1045,25 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
context: *mut dc_context_t,
from_server: libc::c_int,
seconds: i64,
) -> libc::c_int {
if context.is_null() || seconds < 0 {
eprintln!("ignoring careless call to dc_estimate_deletion_cnt()");
return 0;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
message::estimate_deletion_cnt(ctx, from_server != 0, seconds).unwrap_or(0)
as libc::c_int
})
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_fresh_msgs(
context: *mut dc_context_t,
@@ -1455,7 +1474,7 @@ pub unsafe extern "C" fn dc_get_msg_info(
) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_msg_info()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_context = &*context;
ffi_context
@@ -2406,7 +2425,7 @@ pub unsafe extern "C" fn dc_chat_get_type(chat: *mut dc_chat_t) -> libc::c_int {
pub unsafe extern "C" fn dc_chat_get_name(chat: *mut dc_chat_t) -> *mut libc::c_char {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_get_name()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_chat = &*chat;
ffi_chat.chat.get_name().strdup()
@@ -2728,7 +2747,7 @@ pub unsafe extern "C" fn dc_msg_get_sort_timestamp(msg: *mut dc_msg_t) -> i64 {
pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_text()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg.message.get_text().unwrap_or_default().strdup()
@@ -2747,8 +2766,7 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
ffi_msg
.message
.get_file(ctx)
.and_then(|p| p.to_c_string().ok())
.map(|cs| dc_strdup(cs.as_ptr()))
.map(|p| p.strdup())
.unwrap_or_else(|| "".strdup())
})
.unwrap_or_else(|_| "".strdup())
@@ -2758,7 +2776,7 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_filename()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg.message.get_filename().unwrap_or_default().strdup()
@@ -2768,13 +2786,13 @@ pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_filemime()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_msg = &*msg;
if let Some(x) = ffi_msg.message.get_filemime() {
x.strdup()
} else {
dc_strdup(ptr::null())
"".strdup()
}
}
@@ -3098,7 +3116,7 @@ pub unsafe extern "C" fn dc_contact_get_id(contact: *mut dc_contact_t) -> u32 {
pub unsafe extern "C" fn dc_contact_get_addr(contact: *mut dc_contact_t) -> *mut libc::c_char {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_addr()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_contact = &*contact;
ffi_contact.contact.get_addr().strdup()
@@ -3108,7 +3126,7 @@ pub unsafe extern "C" fn dc_contact_get_addr(contact: *mut dc_contact_t) -> *mut
pub unsafe extern "C" fn dc_contact_get_name(contact: *mut dc_contact_t) -> *mut libc::c_char {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_name()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_contact = &*contact;
ffi_contact.contact.get_name().strdup()
@@ -3120,7 +3138,7 @@ pub unsafe extern "C" fn dc_contact_get_display_name(
) -> *mut libc::c_char {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_display_name()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_contact = &*contact;
ffi_contact.contact.get_display_name().strdup()
@@ -3132,7 +3150,7 @@ pub unsafe extern "C" fn dc_contact_get_name_n_addr(
) -> *mut libc::c_char {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_name_n_addr()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_contact = &*contact;
ffi_contact.contact.get_name_n_addr().strdup()
@@ -3144,7 +3162,7 @@ pub unsafe extern "C" fn dc_contact_get_first_name(
) -> *mut libc::c_char {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_first_name()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_contact = &*contact;
ffi_contact.contact.get_first_name().strdup()

View File

@@ -9,7 +9,7 @@ use std::ptr;
/// # Examples
///
/// ```rust,norun
/// use deltachat::dc_tools::{dc_strdup, to_string_lossy};
/// use crate::string::{dc_strdup, to_string_lossy};
/// unsafe {
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
/// let str_a_copy = dc_strdup(str_a);
@@ -17,7 +17,7 @@ use std::ptr;
/// assert_ne!(str_a, str_a_copy);
/// }
/// ```
pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
let ret: *mut libc::c_char;
if !s.is_null() {
ret = libc::strdup(s);
@@ -32,7 +32,7 @@ pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
/// Error type for the [OsStrExt] trait
#[derive(Debug, Fail, PartialEq)]
pub enum CStringError {
pub(crate) enum CStringError {
/// The string contains an interior null byte
#[fail(display = "String contains an interior null byte")]
InteriorNullByte,
@@ -66,7 +66,7 @@ pub enum CStringError {
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
/// }
/// ```
pub trait OsStrExt {
pub(crate) trait OsStrExt {
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
///
/// This is useful to convert e.g. a [std::path::Path] to
@@ -131,15 +131,16 @@ fn os_str_to_c_string_unicode(
}
/// Convenience methods/associated functions for working with [CString]
///
/// This is helps transitioning from unsafe code.
pub trait CStringExt {
/// Create a new [CString], yolo style
trait CStringExt {
/// Create a new [CString], best effort
///
/// This unwrap the result, panicking when there are embedded NULL
/// bytes.
fn yolo<T: Into<Vec<u8>>>(t: T) -> CString {
CString::new(t).expect("String contains null byte, can not be CString")
/// Like the [to_string_lossy] this doesn't give up in the face of
/// bad input (embedded null bytes in this case) instead it does
/// the best it can by stripping the embedded null bytes.
fn new_lossy<T: Into<Vec<u8>>>(t: T) -> CString {
let mut s = t.into();
s.retain(|&c| c != 0);
CString::new(s).unwrap_or_default()
}
}
@@ -151,7 +152,7 @@ impl CStringExt for CString {}
/// Rust strings to raw C strings. This can be clumsy to do correctly
/// and the compiler sometimes allows it in an unsafe way. These
/// methods make it more succinct and help you get it right.
pub trait StrExt {
pub(crate) trait Strdup {
/// Allocate a new raw C `*char` version of this string.
///
/// This allocates a new raw C string which must be freed using
@@ -168,35 +169,44 @@ pub trait StrExt {
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> StrExt for T {
impl<T: AsRef<str>> Strdup for T {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::yolo(self.as_ref());
let tmp = CString::new_lossy(self.as_ref());
dc_strdup(tmp.as_ptr())
}
}
// We can not implement for AsRef<OsStr> because we already implement
// AsRev<str> and this conflicts. So implement for Path directly.
impl Strdup for std::path::Path {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
dc_strdup(tmp.as_ptr())
}
}
/// Convenience methods to turn optional strings into C strings.
///
/// This is the same as the [StrExt] trait but a different trait name
/// to work around the type system not allowing to implement [StrExt]
/// for `Option<impl StrExt>` When we already have an [StrExt] impl
/// This is the same as the [Strdup] trait but a different trait name
/// to work around the type system not allowing to implement [Strdup]
/// for `Option<impl Strdup>` When we already have an [Strdup] impl
/// for `AsRef<&str>`.
///
/// When the [Option] is [Option::Some] this behaves just like
/// [StrExt::strdup], when it is [Option::None] a null pointer is
/// [Strdup::strdup], when it is [Option::None] a null pointer is
/// returned.
pub trait OptStrExt {
pub(crate) trait OptStrdup {
/// Allocate a new raw C `*char` version of this string, or NULL.
///
/// See [StrExt::strdup] for details.
/// See [Strdup::strdup] for details.
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> OptStrExt for Option<T> {
impl<T: AsRef<str>> OptStrdup for Option<T> {
unsafe fn strdup(&self) -> *mut libc::c_char {
match self {
Some(s) => {
let tmp = CString::yolo(s.as_ref());
let tmp = CString::new_lossy(s.as_ref());
dc_strdup(tmp.as_ptr())
}
None => ptr::null_mut(),
@@ -204,7 +214,7 @@ impl<T: AsRef<str>> OptStrExt for Option<T> {
}
}
pub fn to_string_lossy(s: *const libc::c_char) -> String {
pub(crate) fn to_string_lossy(s: *const libc::c_char) -> String {
if s.is_null() {
return "".into();
}
@@ -214,7 +224,7 @@ pub fn to_string_lossy(s: *const libc::c_char) -> String {
cstr.to_string_lossy().to_string()
}
pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
if s.is_null() {
return None;
}
@@ -235,7 +245,7 @@ pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
///
/// [Path]: std::path::Path
#[cfg(not(target_os = "windows"))]
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
assert!(!s.is_null(), "cannot be used on null pointers");
use std::os::unix::ffi::OsStrExt;
unsafe {
@@ -247,7 +257,7 @@ pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
// as_path() implementation for windows, documented above.
#[cfg(target_os = "windows")]
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
as_path_unicode(s)
}
@@ -354,8 +364,14 @@ mod tests {
}
#[test]
fn test_cstring_yolo() {
assert_eq!(CString::new("hello").unwrap(), CString::yolo("hello"));
fn test_cstring_new_lossy() {
assert!(CString::new("hel\x00lo").is_err());
assert!(CString::new(String::from("hel\x00o")).is_err());
let r = CString::new("hello").unwrap();
assert_eq!(CString::new_lossy("hello"), r);
assert_eq!(CString::new_lossy("hel\x00lo"), r);
assert_eq!(CString::new_lossy(String::from("hello")), r);
assert_eq!(CString::new_lossy(String::from("hel\x00lo")), r);
}
#[test]

126
draft/group-sync.rst Normal file
View File

@@ -0,0 +1,126 @@
Problem: missing eventual group consistency
--------------------------------------------
If group members are concurrently adding new members,
the new members will miss each other's additions, example:
- Alice and Bob are in a two-member group
- Alice adds Carol, concurrently Bob adds Doris
- Carol will see a three-member group (Alice, Bob, Carol),
Doris will see a different three-member group (Alice, Bob, Doris),
and only Alice and Bob will have all four members.
Note that for verified groups any mitigation mechanism likely
needs to make all clients to know who originally added a member.
solution: memorize+attach (possible encrypted) chat-meta mime messages
----------------------------------------------------------------------
For reference, please see https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members how MemberAdded/Removed messages are shaped.
- All Chat-Group-Member-Added/Removed messages are recorded in their
full raw (signed and encrypted) mime-format in the DB
- If an incoming member-add/member-delete messages has a member list
which is, apart from the added/removed member, not consistent
with our own view, broadcast a "Chat-Group-Member-Correction" message to
all members, attaching the original added/removed mime-message for all mismatching
contacts. If we have no relevant add/del information, don't send a
correction message out.
- Upong receiving added/removed attachments we don't do the
check_consistency+correction message dance.
This avoids recursion problems and hard-to-reason-about chatter.
Notes:
- mechanism works for both encrypted and unencrypted add/del messages
- we already have a "mime_headers" column in the DB for each incoming message.
We could extend it to also include the payload and store mime unconditionally
for member-added/removed messages.
- multiple member-added/removed messages can be attached in a single
correction message
- it is minimal on the number of overall messages to reach group consistency
(best-case: no extra messages, the ABCD case above: max two extra messages)
- somewhat backward compatible: older clients will probably ignore
messages which are signed by someone who is not the outer From-address.
- the correction-protocol also helps with dropped messages. If a member
did not see a member-added/removed message, the next member add/removed
message in the group will likely heal group consistency for this member.
- we can quite easily extend the mechanism to also provide the group-avatar or
other meta-information.
Discussions of variants
++++++++++++++++++++++++
- instead of acting on MemberAdded/Removed message we could send
corrections for any received message that addresses inconsistent group members but
a) this would delay group-membership healing
b) could lead to a lot of members sending corrections
- instead of broadcasting correction messages we could only send it to
the sender of the inconsistent member-added/removed message.
A receiver of such a correction message would then need to forward
the message to the members it thinks also have an inconsistent view.
This sounds complicated and error-prone. Concretely, if Alice
receives Bob's "Member-added: Doris" message, then Alice
broadcasting the correction message with "Member-added: Carol"
would reach all four members, healing group consistency in one step.
If Bob meanwhile receives Alice's "Member-Added: Carol" message,
Bob would broadcast a correction message to all four members as well.
(Imagine a situation where Alice/Bob added Carol/Doris
while both being in an offline or bad-connection situation).
solution2: repeat member-added/removed messages
---------------------------------------------------
Introduce a new Chat-Group-Member-Changed header and deprecate Chat-Group-Member-Added/Removed
but keep sending out the old headers until the new protocol is sufficiently deployed.
The new Chat-Group-Member-Changed header contains a Time-to-Live number (TTL)
which controls repetition of the signed "add/del e-mail address" payload.
Example::
Chat-Group-Member-Changed: TTL add "somedisplayname" someone@example.org
owEBYQGe/pANAwACAY47A6J5t3LWAcsxYgBeTQypYWRkICJzb21lZGlzcGxheW5h
bWUiIHNvbWVvbmVAZXhhbXBsZS5vcmcgCokBHAQAAQIABgUCXk0MqQAKCRCOOwOi
ebdy1hfRB/wJ74tgFQulicthcv9n+ZsqzwOtBKMEVIHqJCzzDB/Hg/2z8ogYoZNR
iUKKrv3Y1XuFvdKyOC+wC/unXAWKFHYzY6Tv6qDp6r+amt+ad+8Z02q53h9E55IP
FUBdq2rbS8hLGjQB+mVRowYrUACrOqGgNbXMZjQfuV7fSc7y813OsCQgi3tjstup
b+uduVzxCp3PChGhcZPs3iOGCnQvSB8VAaLGMWE2d7nTo/yMQ0Jx69x5qwfXogTk
mTt5rOJyrosbtf09TMKFzGgtqBcEqHLp3+mQpZQ+WHUKAbsRa8Jc9DOUOSKJ8SNM
clKdskprY+4LY0EBwLD3SQ7dPkTITCRD
=P6GG
TTL is set to "2" on an initial Chat-Group-Member-Changed add/del message.
Receivers will apply the add/del change to the group-membership,
decrease the TTL by 1, and if TTL>0 re-sent the header.
The "add|del e-mail address" payload is pgp-signed and repeated verbatim.
This allows to propagate, in a cryptographically secured way,
who added a member. This is particularly important for allowing
to show in verified groups who added a member (planned).
Disadvantage to solution 1:
- requires to specify encoding and precise rules for what/how is signed.
- causes O(N^2) extra messages
- Not easily extendable for other things (without introducing a new
header / encoding)

View File

@@ -1,5 +1,6 @@
use std::path::Path;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
use deltachat::chatlist::*;
@@ -9,6 +10,7 @@ use deltachat::context::*;
use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::error::Error;
use deltachat::export_chat::{export_chat, pack_exported_chat};
use deltachat::imex::*;
use deltachat::job::*;
use deltachat::location;
@@ -374,6 +376,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
pin <chat-id>\n\
unpin <chat-id>\n\
delchat <chat-id>\n\
export-chat <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
msginfo <msg-id>\n\
@@ -397,6 +400,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
providerinfo <addr>\n\
event <event-id to test>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
clear -- clear screen\n\
exit or quit\n\
@@ -867,6 +871,24 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let chat_id = ChatId::new(arg1.parse()?);
chat_id.delete(context)?;
}
"export-chat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
let res = export_chat(context, chat_id);
println!("{:?}", res);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let destination_raw = context.get_blobdir().join(format!(
"exported_{}_{}.zip",
chat_id.to_u32(),
timestamp
));
let destination = destination_raw.to_str().unwrap();
let pack_res = pack_exported_chat(context, res, destination);
println!("{:?} - destination: {}", pack_res, destination);
}
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
@@ -1028,6 +1050,16 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
bail!("Command failed.");
}
}
"estimatedeletion" => {
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
let seconds = arg1.parse()?;
let device_cnt = message::estimate_deletion_cnt(context, false, seconds)?;
let server_cnt = message::estimate_deletion_cnt(context, true, seconds)?;
println!(
"estimated count of messages older than {} seconds:\non device: {}\non server: {}",
seconds, device_cnt, server_cnt
);
}
"emptyserver" => {
ensure!(!arg1.is_empty(), "Argument <flags> missing");

View File

@@ -262,7 +262,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 26] = [
const CHAT_COMMANDS: [&str; 27] = [
"listchats",
"listarchived",
"chat",
@@ -289,6 +289,7 @@ const CHAT_COMMANDS: [&str; 26] = [
"pin",
"unpin",
"delchat",
"export-chat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
"listmsgs",
@@ -308,8 +309,17 @@ const CONTACT_COMMANDS: [&str; 6] = [
"delcontact",
"cleanupcontacts",
];
const MISC_COMMANDS: [&str; 9] = [
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
const MISC_COMMANDS: [&str; 10] = [
"getqr",
"getbadqr",
"checkqr",
"event",
"fileinfo",
"clear",
"exit",
"quit",
"help",
"estimatedeletion",
];
impl Hinter for DcHelper {

View File

@@ -76,6 +76,20 @@ class TestOfflineAccountBasic:
with pytest.raises(KeyError):
ac1.get_config("123123")
def test_empty_group_bcc_self_enabled(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
ac1.set_config("bcc_self", "1")
chat = ac1.create_group_chat(name="group1")
msg = chat.send_text("msg1")
assert msg in chat.get_messages()
def test_empty_group_bcc_self_disabled(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
ac1.set_config("bcc_self", "0")
chat = ac1.create_group_chat(name="group1")
msg = chat.send_text("msg1")
assert msg in chat.get_messages()
class TestOfflineContact:
def test_contact_attr(self, acfactory):
@@ -1366,6 +1380,80 @@ class TestGroupStressTests:
# Message should be encrypted because keys of other members are gossiped
assert msg.is_encrypted()
def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp):
"""
Test that user recreates group member list when it joins the group again.
ac1 creates a group with two other accounts: ac2 and ac3
Then it removes ac2, removes ac3 and adds ac2 back.
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
"""
lp.sec("creating and configuring five accounts")
accounts = [acfactory.get_online_configuring_account() for i in range(3)]
for acc in accounts:
wait_configuration_progress(acc, 1000)
ac1 = accounts.pop()
lp.sec("ac1: setting up contacts with 2 other members")
contacts = []
for acc, name in zip(accounts, ["ac2", "ac3"]):
contact = ac1.create_contact(acc.get_config("addr"), name=name)
contacts.append(contact)
# make sure we accept the "hi" message
ac1.create_chat_by_contact(contact)
# make sure the other side accepts our messages
c1 = acc.create_contact(ac1.get_config("addr"), "a member")
chat1 = acc.create_chat_by_contact(c1)
# send a message to get the contact key via autocrypt header
chat1.send_text("hi")
msg = ac1.wait_next_incoming_message()
assert msg.text == "hi"
ac2, ac3 = accounts
lp.sec("ac1: creating group chat with 2 other members")
chat = ac1.create_group_chat("title1")
for contact in contacts:
chat.add_contact(contact)
assert not chat.is_promoted()
lp.sec("ac1: send mesage to new group chat")
msg = chat.send_text("hello")
assert chat.is_promoted()
assert msg.is_encrypted()
num_contacts = len(chat.get_contacts())
assert num_contacts == 3
lp.sec("checking that the chat arrived correctly")
for ac in accounts:
msg = ac.wait_next_incoming_message()
assert msg.text == "hello"
print("chat is", msg.chat)
assert len(msg.chat.get_contacts()) == 3
lp.sec("ac1: removing ac2")
chat.remove_contact(contacts[0])
lp.sec("ac2: wait for a message about removal from the chat")
msg = ac2.wait_next_incoming_message()
lp.sec("ac1: removing ac3")
chat.remove_contact(contacts[1])
lp.sec("ac1: adding ac2 back")
# Group is promoted, message is sent automatically
assert chat.is_promoted()
chat.add_contact(contacts[0])
lp.sec("ac2: check that ac3 is removed")
msg = ac2.wait_next_incoming_message()
assert len(msg.chat.get_contacts()) == len(chat.get_contacts())
class TestOnlineConfigureFails:
def test_invalid_password(self, acfactory):

View File

@@ -14,6 +14,7 @@ passenv =
DCC_RS_DEV
DCC_RS_TARGET
DCC_PY_LIVECONFIG
DCC_NEW_TMP_EMAIL
CARGO_TARGET_DIR
RUSTC_WRAPPER
deps =

View File

@@ -75,7 +75,7 @@ impl Aheader {
wanted_from: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Option<Self> {
if let Ok(Some(value)) = headers.get_header_value(HeaderDef::Autocrypt) {
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
match Self::from_str(&value) {
Ok(header) => {
if addr_cmp(&header.addr, wanted_from) {

View File

@@ -286,10 +286,7 @@ impl ChatId {
/// Returns `true`, if message was deleted, `false` otherwise.
fn maybe_delete_draft(self, context: &Context) -> bool {
match self.get_draft_msg_id(context) {
Some(msg_id) => {
Message::delete_from_db(context, msg_id);
true
}
Some(msg_id) => msg_id.delete_from_db(context).is_ok(),
None => false,
}
}
@@ -362,6 +359,25 @@ impl ChatId {
.unwrap_or_default() as usize
}
pub(crate) fn get_param(self, context: &Context) -> Result<Params, Error> {
let res: Option<String> = context
.sql
.query_get_value_result("SELECT param FROM chats WHERE id=?", params![self])?;
Ok(res
.map(|s| s.parse().unwrap_or_default())
.unwrap_or_default())
}
// Returns true if chat is a saved messages chat.
pub fn is_self_talk(self, context: &Context) -> Result<bool, Error> {
Ok(self.get_param(context)?.exists(Param::Selftalk))
}
/// Returns true if chat is a device chat.
pub fn is_device_talk(self, context: &Context) -> Result<bool, Error> {
Ok(self.get_param(context)?.exists(Param::Devicetalk))
}
/// Bad evil escape hatch.
///
/// Avoid using this, eventually types should be cleaned up enough
@@ -1450,6 +1466,18 @@ pub fn get_chat_msgs(
flags: u32,
marker1before: Option<MsgId>,
) -> Vec<MsgId> {
match hide_device_expired_messages(context) {
Err(err) => warn!(context, "Failed to delete expired messages: {}", err),
Ok(messages_deleted) => {
if messages_deleted {
context.call_cb(Event::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
})
}
}
}
let process_row =
|row: &rusqlite::Row| Ok((row.get::<_, MsgId>("id")?, row.get::<_, i64>("timestamp")?));
let process_rows = |rows: rusqlite::MappedRows<_>| {
@@ -1584,6 +1612,47 @@ pub fn marknoticed_all_chats(context: &Context) -> Result<(), Error> {
Ok(())
}
/// Hides messages which are expired according to "delete_device_after" setting.
///
/// Returns true if any message is hidden, so event can be emitted. If nothing
/// has been hidden, returns false.
pub fn hide_device_expired_messages(context: &Context) -> Result<bool, Error> {
if let Some(delete_device_after) = context.get_config_delete_device_after() {
let threshold_timestamp = time() - delete_device_after;
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.unwrap_or_default()
.0;
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.unwrap_or_default()
.0;
// Hide expired messages
//
// Only update the rows that have to be updated, to avoid emitting
// unnecessary "chat modified" events.
let rows_modified = context.sql.execute(
"UPDATE msgs \
SET txt = 'DELETED', hidden = 1 \
WHERE timestamp < ? \
AND chat_id > ? \
AND chat_id != ? \
AND chat_id != ? \
AND NOT hidden",
params![
threshold_timestamp,
DC_CHAT_ID_LAST_SPECIAL,
self_chat_id,
device_chat_id
],
)?;
Ok(rows_modified > 0)
} else {
Ok(false)
}
}
pub fn get_chat_media(
context: &Context,
chat_id: ChatId,
@@ -2063,7 +2132,7 @@ pub fn remove_contact_from_chat(
"Cannot remove contact from chat; self not in group.".into()
)
);
} else {
} else if remove_from_chat_contacts_table(context, chat_id, contact_id) {
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
if let Ok(contact) = Contact::get_by_id(context, contact_id) {
if chat.is_promoted() {
@@ -2093,10 +2162,9 @@ pub fn remove_contact_from_chat(
});
}
}
if remove_from_chat_contacts_table(context, chat_id, contact_id) {
context.call_cb(Event::ChatModified(chat_id));
success = true;
}
context.call_cb(Event::ChatModified(chat_id));
success = true;
}
}
}

View File

@@ -1,5 +1,6 @@
//! # Chat list module
use crate::chat;
use crate::chat::*;
use crate::constants::*;
use crate::contact::*;
@@ -73,6 +74,9 @@ impl Chatlist {
/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
/// chats
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
/// and hides the device-chat,
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
@@ -88,6 +92,12 @@ impl Chatlist {
query: Option<&str>,
query_contact_id: Option<u32>,
) -> Result<Self> {
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
// messages get hidden to avoid reloading the same chatlist.
if let Err(err) = hide_device_expired_messages(context) {
warn!(context, "Failed to hide expired messages: {}", err);
}
let mut add_archived_link_item = false;
let process_row = |row: &rusqlite::Row| {
@@ -101,6 +111,14 @@ impl Chatlist {
.map_err(Into::into)
};
let skip_id = if 0 != listflags & DC_GCL_FOR_FORWARDING {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.unwrap_or_default()
.0
} else {
ChatId::new(0)
};
// select with left join and minimum:
//
// - the inner select must use `hidden` and _not_ `m.hidden`
@@ -139,6 +157,9 @@ impl Chatlist {
)?
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
// show archived chats
// (this includes the archived device-chat; we could skip it,
// however, then the number of archived chats do not match, which might be even more irritating.
// and adapting the number requires larger refactorings and seems not to be worth the effort)
context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
@@ -178,18 +199,25 @@ impl Chatlist {
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?))
WHERE c.id>9
AND (hidden=0 OR state=?1))
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND c.name LIKE ?
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, str_like_cmd],
params![MessageState::OutDraft, skip_id, str_like_cmd],
process_row,
process_rows,
)?
} else {
// show normal chatlist
let sort_id_up = if 0 != listflags & DC_GCL_FOR_FORWARDING {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.unwrap_or_default()
.0
} else {
ChatId::new(0)
};
let mut ids = context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
@@ -200,21 +228,23 @@ impl Chatlist {
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1))
WHERE c.id>9
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND NOT c.archived=?2
AND NOT c.archived=?3
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, ChatVisibility::Archived, ChatVisibility::Pinned],
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
process_row,
process_rows,
)?;
if 0 == listflags & DC_GCL_NO_SPECIALS {
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) {
ids.insert(
0,
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
);
if 0 == listflags & DC_GCL_FOR_FORWARDING {
ids.insert(
0,
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
);
}
}
add_archived_link_item = true;
}
@@ -399,6 +429,25 @@ mod tests {
assert_eq!(chats.len(), 1);
}
#[test]
fn test_sort_self_talk_up_on_forward() {
let t = dummy_context();
t.ctx.update_device_chats().unwrap();
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert!(chats.len() == 3);
assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
.unwrap()
.is_self_talk());
let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None).unwrap();
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
assert!(Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
.unwrap()
.is_self_talk());
}
#[test]
fn test_search_special_chat_names() {
let t = dummy_context();

View File

@@ -4,10 +4,13 @@ use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::*;
use crate::events::Event;
use crate::job::*;
use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::stock::StockMessage;
use rusqlite::NO_PARAMS;
@@ -65,6 +68,25 @@ pub enum Config {
#[strum(props(default = "0"))]
KeyGenType,
/// Timer in seconds after which the message is deleted from the
/// server.
///
/// Equals to 0 by default, which means the message is never
/// deleted.
///
/// Value 1 is treated as "delete at once": messages are deleted
/// immediately, without moving to DeltaChat folder.
#[strum(props(default = "0"))]
DeleteServerAfter,
/// Timer in seconds after which the message is deleted from the
/// device.
///
/// Equals to 0 by default, which means the message is never
/// deleted.
#[strum(props(default = "0"))]
DeleteDeviceAfter,
SaveMimeHeaders,
ConfiguredAddr,
ConfiguredMailServer,
@@ -128,6 +150,29 @@ impl Context {
self.get_config_int(key) != 0
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
/// at once, `Some(x)` means delete after `x` seconds.
pub fn get_config_delete_server_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteServerAfter) {
0 => None,
1 => Some(0),
x => Some(x as i64),
}
}
/// Gets configured "delete_device_after" value.
///
/// `None` means never delete the message, `Some(x)` means delete
/// after `x` seconds.
pub fn get_config_delete_device_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteDeviceAfter) {
0 => None,
x => Some(x as i64),
}
}
/// Set the given config key.
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
pub fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
@@ -171,6 +216,15 @@ impl Context {
self.sql.set_raw_config(self, key, val)
}
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(self, key, value);
// Force chatlist reload to delete old messages immediately.
self.call_cb(Event::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
ret
}
_ => self.sql.set_raw_config(self, key, value),
}
}

View File

@@ -371,7 +371,7 @@ pub(crate) fn JobConfigureImap(context: &Context) -> job::Status {
let create_mvbox = context.get_config_bool(Config::MvboxWatch)
|| context.get_config_bool(Config::MvboxMove);
let imap = &context.inbox_thread.read().unwrap().imap;
if let Err(err) = imap.ensure_configured_folders(context, create_mvbox) {
if let Err(err) = imap.configure_folders(context, create_mvbox) {
warn!(context, "configuring folders failed: {:?}", err);
false
} else {

View File

@@ -80,6 +80,7 @@ pub(crate) const DC_FROM_HANDSHAKE: i32 = 0x01;
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
@@ -213,6 +214,9 @@ pub const DC_BOB_SUCCESS: i32 = 1;
// max. width/height of an avatar
pub const AVATAR_SIZE: u32 = 192;
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
#[derive(
Debug,
Display,

View File

@@ -1067,7 +1067,7 @@ pub fn normalize_name(full_name: impl AsRef<str>) -> String {
}
let len = full_name.len();
if len > 0 {
if len > 1 {
let firstchar = full_name.as_bytes()[0];
let lastchar = full_name.as_bytes()[len - 1];
if firstchar == b'\'' && lastchar == b'\''
@@ -1176,6 +1176,10 @@ mod tests {
fn test_normalize_name() {
assert_eq!(&normalize_name("Doe, John"), "John Doe");
assert_eq!(&normalize_name(" hello world "), "hello world");
assert_eq!(&normalize_name("<"), "<");
assert_eq!(&normalize_name(">"), ">");
assert_eq!(&normalize_name("'"), "'");
assert_eq!(&normalize_name("\""), "\"");
}
#[test]

View File

@@ -17,7 +17,7 @@ use crate::message::{self, MessageState, MessengerMessage, MsgId};
use crate::mimeparser::*;
use crate::param::*;
use crate::peerstate::*;
use crate::securejoin::{self, handle_securejoin_handshake};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::sql;
use crate::stock::StockMessage;
use crate::{contact, location};
@@ -192,17 +192,24 @@ pub fn dc_receive_imf(
};
}
// if we delete we don't need to try moving messages
if needs_delete_job && !created_db_entries.is_empty() {
job_add(
context,
Action::DeleteMsgOnImap,
created_db_entries[0].1.to_u32() as i32,
Params::new(),
0,
);
} else {
context.do_heuristics_moves(server_folder.as_ref(), insert_msg_id);
// Get user-configured server deletion
let delete_server_after = context.get_config_delete_server_after();
if !created_db_entries.is_empty() {
if needs_delete_job || delete_server_after == Some(0) {
for db_entry in &created_db_entries {
job_add(
context,
Action::DeleteMsgOnImap,
db_entry.1.to_u32() as i32,
Params::new(),
0,
);
}
} else {
// Move message if we don't delete it immediately.
context.do_heuristics_moves(server_folder.as_ref(), insert_msg_id);
}
}
info!(
@@ -212,7 +219,7 @@ pub fn dc_receive_imf(
cleanup(context, &create_event_to_send, created_db_entries);
mime_parser.handle_reports(context, from_id, sent_timestamp, &server_folder, server_uid);
mime_parser.handle_reports(context, from_id, sent_timestamp);
Ok(())
}
@@ -340,11 +347,9 @@ fn add_parts(
};
to_id = DC_CONTACT_ID_SELF;
// handshake messages must be processed _before_ chats are created
// (eg. contacs may be marked as verified)
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get(HeaderDef::SecureJoin).is_some() {
// avoid discarding by show_emails setting
msgrmsg = MessengerMessage::Yes;
msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting
*chat_id = ChatId::new(0);
allow_creation = true;
match handle_securejoin_handshake(context, mime_parser, from_id) {
@@ -358,8 +363,7 @@ fn add_parts(
state = MessageState::InSeen;
}
Ok(securejoin::HandshakeMessage::Propagate) => {
// Message will still be processed as "member
// added" or similar system message.
// process messages as "member added" normally
}
Err(err) => {
*hidden = true;
@@ -389,7 +393,11 @@ fn add_parts(
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_group(
context,
&mut mime_parser,
allow_creation,
if test_normal_chat_id.is_unset() {
allow_creation
} else {
true
},
create_blocked,
from_id,
to_ids,
@@ -472,6 +480,27 @@ fn add_parts(
// We cannot recreate other states (read, error).
state = MessageState::OutDelivered;
to_id = to_ids.get_index(0).cloned().unwrap_or_default();
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get(HeaderDef::SecureJoin).is_some() {
msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting
*chat_id = ChatId::new(0);
allow_creation = true;
match observe_securejoin_on_other_device(context, mime_parser, to_id) {
Ok(securejoin::HandshakeMessage::Done)
| Ok(securejoin::HandshakeMessage::Ignore) => {
*hidden = true;
}
Ok(securejoin::HandshakeMessage::Propagate) => {
// process messages as "member added" normally
}
Err(err) => {
*hidden = true;
error!(context, "Error in Secure-Join watching: {}", err);
}
}
}
if !to_ids.is_empty() {
if chat_id.is_unset() {
let (new_chat_id, new_chat_id_blocked) = create_or_lookup_group(
@@ -579,10 +608,13 @@ fn add_parts(
let subject = mime_parser.get_subject().unwrap_or_default();
for part in mime_parser.parts.iter_mut() {
if mime_parser.location_kml.is_some()
let is_mdn = !mime_parser.reports.is_empty();
let is_location_kml = mime_parser.location_kml.is_some()
&& icnt == 1
&& (part.msg == "-location-" || part.msg.is_empty())
{
&& (part.msg == "-location-" || part.msg.is_empty());
if is_mdn || is_location_kml {
*hidden = true;
if state == MessageState::InFresh {
state = MessageState::InNoticed;
@@ -1023,6 +1055,17 @@ fn create_or_lookup_group(
// add members to group/check members
if recreate_member_list {
if !chat::is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) {
// Members could have been removed while we were
// absent. We can't use existing member list and need to
// start from scratch.
sql::execute(
context,
&context.sql,
"DELETE FROM chats_contacts WHERE chat_id=?;",
params![chat_id],
)
.ok();
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF);
}
if from_id > DC_CONTACT_ID_LAST_SPECIAL
@@ -1477,6 +1520,7 @@ fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
/// Check if a message is a reply to a known message (messenger or non-messenger).
fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
context
.sql
.exists(
@@ -1523,6 +1567,7 @@ pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -
/// Check if a message is a reply to any messenger message.
fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
context
.sql
.exists(
@@ -1612,7 +1657,9 @@ fn dc_create_incoming_rfc724_mid(
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::dummy_context;
use crate::chatlist::Chatlist;
use crate::message::Message;
use crate::test_utils::{dummy_context, TestContext};
#[test]
fn test_hex_hash() {
@@ -1671,4 +1718,170 @@ mod tests {
Some("123-45-9@stub".into())
);
}
#[test]
fn test_is_known_rfc724_mid() {
let t = dummy_context();
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("first message".to_string());
let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap();
let msg = Message::load_from_db(&t.ctx, msg_id).unwrap();
// Message-IDs may or may not be surrounded by angle brackets
assert!(is_known_rfc724_mid(
&t.ctx,
format!("<{}>", msg.rfc724_mid).as_str()
));
assert!(is_known_rfc724_mid(&t.ctx, &msg.rfc724_mid));
assert!(!is_known_rfc724_mid(&t.ctx, "nonexistant@message.id"));
}
#[test]
fn test_is_msgrmsg_rfc724_mid() {
let t = dummy_context();
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("first message".to_string());
let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap();
let msg = Message::load_from_db(&t.ctx, msg_id).unwrap();
// Message-IDs may or may not be surrounded by angle brackets
assert!(is_msgrmsg_rfc724_mid(
&t.ctx,
format!("<{}>", msg.rfc724_mid).as_str()
));
assert!(is_msgrmsg_rfc724_mid(&t.ctx, &msg.rfc724_mid));
assert!(!is_msgrmsg_rfc724_mid(&t.ctx, "nonexistant@message.id"));
}
fn configured_offline_context() -> TestContext {
let t = dummy_context();
t.ctx
.set_config(Config::Addr, Some("alice@example.org"))
.unwrap();
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
.unwrap();
t.ctx.set_config(Config::Configured, Some("1")).unwrap();
t
}
static MSGRMSG: &[u8] = b"From: Bob <bob@example.org>\n\
To: alice@example.org\n\
Chat-Version: 1.0\n\
Subject: Chat: hello\n\
Message-ID: <Mr.1111@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:55 +0000\n\
\n\
hello\n";
static ONETOONE_NOREPLY_MAIL: &[u8] = b"From: Bob <bob@example.org>\n\
To: alice@example.org\n\
Subject: Chat: hello\n\
Message-ID: <2222@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n";
static GRP_MAIL: &[u8] = b"From: bob@example.org\n\
To: alice@example.org, claire@example.org\n\
Subject: group with Alice, Bob and Claire\n\
Message-ID: <3333@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n";
#[test]
fn test_adhoc_group_show_chats_only() {
let t = configured_offline_context();
assert_eq!(t.ctx.get_config_int(Config::ShowEmails), 0);
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 0);
dc_receive_imf(&t.ctx, MSGRMSG, "INBOX", 1, false).unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 1);
dc_receive_imf(&t.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 1, false).unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 1);
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 1);
}
#[test]
fn test_adhoc_group_show_accepted_contact_unknown() {
let t = configured_offline_context();
t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap();
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
// adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 0);
}
#[test]
fn test_adhoc_group_show_accepted_contact_known() {
let t = configured_offline_context();
t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap();
Contact::create(&t.ctx, "Bob", "bob@example.org").unwrap();
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
// adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts
// (and existent chat is required)
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 0);
}
#[test]
fn test_adhoc_group_show_accepted_contact_accepted() {
let t = configured_offline_context();
t.ctx.set_config(Config::ShowEmails, Some("1")).unwrap();
// accept Bob by accepting a delta-message from Bob
dc_receive_imf(&t.ctx, MSGRMSG, "INBOX", 1, false).unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 1);
assert!(chats.get_chat_id(0).is_deaddrop());
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
assert!(!chat_id.is_special());
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap();
assert_eq!(chat.typ, Chattype::Single);
assert_eq!(chat.name, "Bob");
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 1);
assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).len(), 1);
// receive a non-delta-message from Bob, shows up because of the show_emails setting
dc_receive_imf(&t.ctx, ONETOONE_NOREPLY_MAIL, "INBOX", 2, false).unwrap();
assert_eq!(chat::get_chat_msgs(&t.ctx, chat_id, 0, None).len(), 2);
// let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 3, false).unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 2);
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap();
assert_eq!(chat.typ, Chattype::Group);
assert_eq!(chat.name, "group with Alice, Bob and Claire");
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 3);
}
#[test]
fn test_adhoc_group_show_all() {
let t = configured_offline_context();
t.ctx.set_config(Config::ShowEmails, Some("2")).unwrap();
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false).unwrap();
// adhoc-group with unknown contacts with show_emails=all will show up in the deaddrop
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 1);
assert!(chats.get_chat_id(0).is_deaddrop());
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap()).unwrap();
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).unwrap();
assert_eq!(chat.typ, Chattype::Group);
assert_eq!(chat.name, "group with Alice, Bob and Claire");
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).len(), 3);
}
}

View File

@@ -126,7 +126,7 @@ pub fn try_decrypt(
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
let from = mail
.headers
.get_header_value(HeaderDef::From_)?
.get_header_value(HeaderDef::From_)
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
@@ -417,7 +417,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
}
#[test]
#[ignore] // generating keys is expensive
fn test_generate() {
let t = dummy_context();
let addr = "alice@example.org";
@@ -429,7 +428,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
}
#[test]
#[ignore]
fn test_generate_concurrent() {
use std::sync::Arc;
use std::thread;

345
src/export_chat.rs Normal file
View File

@@ -0,0 +1,345 @@
// use crate::dc_tools::*;
use crate::chat::*;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::*;
use crate::context::Context;
use crate::error::Error;
use crate::message::*;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use zip::write::FileOptions;
#[derive(Debug)]
pub struct ExportChatResult {
html: String,
referenced_blobs: Vec<String>,
}
struct ContactInfo {
name: String,
initial: String,
color: String,
profile_img: Option<String>,
}
pub fn pack_exported_chat(
context: &Context,
artifact: ExportChatResult,
filename: &str,
) -> zip::result::ZipResult<()> {
let path = std::path::Path::new(filename);
let file = std::fs::File::create(&path).unwrap();
let mut zip = zip::ZipWriter::new(file);
zip.start_file("index.html", Default::default())?;
zip.write_all(artifact.html.as_bytes())?;
zip.start_file("styles.css", Default::default())?;
zip.write_all(include_bytes!("../assets/exported-chat.css"))?;
zip.add_directory("blobs/", Default::default())?;
let options = FileOptions::default();
for blob_name in artifact.referenced_blobs {
let path = context.get_blobdir().join(&blob_name);
// println!("adding file {:?} as {:?} ...", path, &blob_name);
zip.start_file_from_path(Path::new(&format!("blobs/{}", &blob_name)), options)?;
let mut f = File::open(path)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
zip.write_all(&*buffer)?;
buffer.clear();
}
zip.finish()?;
Ok(())
}
pub fn export_chat(context: &Context, chat_id: ChatId) -> ExportChatResult {
let mut blobs = Vec::new();
let mut chat_author_ids = Vec::new();
// get all messages
let messages: Vec<std::result::Result<Message, Error>> =
get_chat_msgs(context, chat_id, 0, None)
.into_iter()
.map(|msg_id| Message::load_from_db(context, msg_id))
.collect();
// push all referenced blobs and populate contactid list
for message in &messages {
if let Ok(msg) = &message {
let filename = msg.get_filename();
if let Some(file) = filename {
// push referenced blobs (attachments)
blobs.push(file);
}
chat_author_ids.push(msg.from_id);
}
}
// deduplicate contact list and load the contacts
chat_author_ids.dedup();
// chache information about the authors
let mut chat_authors: HashMap<u32, ContactInfo> = HashMap::new();
chat_authors.insert(
0,
ContactInfo {
name: "Err: Contact not found".to_owned(),
initial: "#".to_owned(),
profile_img: None,
color: "grey".to_owned(),
},
);
for author_id in chat_author_ids {
let contact = Contact::get_by_id(context, author_id);
if let Ok(c) = contact {
let profile_img_path: String;
if let Some(path) = c.get_profile_image(context) {
profile_img_path = path
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
.to_owned();
// push referenced blobs (avatars)
blobs.push(profile_img_path.clone());
} else {
profile_img_path = "".to_owned();
}
chat_authors.insert(
author_id,
ContactInfo {
name: c.get_display_name().to_owned(),
initial: "#".to_owned(), // TODO
profile_img: match profile_img_path != "" {
true => Some(profile_img_path),
false => None,
},
color: "rgb(18, 126, 208)".to_owned(), // TODO
},
);
}
}
// run message_to_html for each message and generate the html that way
let mut html_messages: Vec<String> = Vec::new();
for message in messages {
if let Ok(msg) = message {
html_messages.push(message_to_html(&chat_authors, msg, context));
} else {
html_messages.push(format!(
r#"<li>
<div class='message error'>
<div class="msg-container">
<div class="msg-body">
<div dir="auto" class="text">{:?}</div>
</div>
</div>
</div>
</li>"#,
message.unwrap_err()
));
}
}
// todo chat image, chat name and so on..
let chat = Chat::load_from_db(context, chat_id).unwrap();
let chat_avatar = match chat.get_profile_image(context) {
Some(img) => {
let path = img
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
.to_owned();
blobs.push(path.clone());
format!("<img class=\"avatar\" src=\"blobs/{}\" />", path)
}
None => format!(
"<div class=\"avatar text-avatar\" style=\"background-color:#{:#}\">{}</div>",
chat.get_color(context),
chat.get_name().chars().next().unwrap()
),
};
// todo option to export locations as kml?
// todo export message infos and save them to txt files
// (those can be linked from the messages, they are stored in msg_info/[msg-id].txt)
blobs.dedup();
ExportChatResult {
html: format!(
"<html>\
<head>\
<title>{chat_name}</title>\
<link rel=\"stylesheet\" href=\"styles.css\" type=\"text/css\">\
</head>\
<body>\
<div class=\"header\">\
{chat_avatar}\
<div class=\"name\">{chat_name}</div>\
</div>\
<div class=\"message-list-and-composer__message-list\">\
<div id=\"message-list\">\
<ul>{messages}</ul>\
</div>\
</div>\
</body>\
</html>",
chat_name = chat.get_name(),
chat_avatar = chat_avatar,
messages = html_messages.join("")
),
referenced_blobs: blobs,
}
}
fn message_to_html(
author_cache: &HashMap<u32, ContactInfo>,
message: Message,
context: &Context,
) -> String {
let author: &ContactInfo = {
if let Some(c) = author_cache.get(&message.get_from_id()) {
c
} else {
author_cache.get(&0).unwrap()
}
};
let avatar: String = {
if let Some(profile_img) = &author.profile_img {
format!(
"<div class=\"author-avatar\">\
<img \
alt=\"{author_name}\"\
src=\"blobs/{author_avatar_src}\"\
/>\
</div>",
author_name = author.name,
author_avatar_src = profile_img
)
} else {
format!(
"<div class=\"author-avatar default\" alt=\"{name}\">\
<div class=\"label\" style=\"background-color: {color}\">\
{initial}\
</div>\
</div>",
name = author.name,
initial = author.initial,
color = author.color
)
}
};
// save and refernce message source code somehow?
let has_text = message.get_text().is_some() && !message.get_text().unwrap().is_empty();
let attachment = match message.get_file(context) {
None => "".to_owned(),
Some(file) => {
let modifier_class = if has_text { "content-below" } else { "" };
let filename = file
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
.to_owned();
match message.get_viewtype() {
Viewtype::Audio => {
format!("<audio \
controls \
class=\"message-attachment-audio {}\"> \
<source src=\"blobs/{}\" /> \
</audio>", modifier_class ,filename)
},
Viewtype::Gif | Viewtype::Image | Viewtype::Sticker => {
format!("<a \
href=\"blobs/{filename}\" \
role=\"button\" \
class=\"message-attachment-media {modifier_class}\"> \
<img className='attachment-content' src=\"blobs/{filename}\" /> \
</a>", modifier_class=modifier_class, filename=filename)
},
Viewtype::Video => {
format!("<a \
href=\"blobs/{filename}\" \
role=\"button\" \
class=\"message-attachment-media {modifier_class}\"> \
<video className='attachment-content' src=\"blobs/{filename}\" controls=\"true\" /> \
</a>", modifier_class=modifier_class, filename=filename)
},
_ => {
format!("<div class=\"message-attachment-generic {modifier_class}\">\
<div class=\"file-icon\">\
<div class=\"file-extension\">\
{extension} \
</div>\
</div>\
<div className=\"text-part\">\
<a href=\"blobs/{filename}\" className=\"name\">{filename}</a>\
<div className=\"size\">{filesize}</div>\
</div>\
</div>",
modifier_class=modifier_class,
filename=filename,
filesize=message.get_filebytes(&context) /* todo human readable file size*/,
extension=file.extension().unwrap_or_else(|| std::ffi::OsStr::new("")).to_str().unwrap().to_owned())
}
}
}
};
format!(
"<li>\
<div class=\"message {direction}\">\
{avatar}\
<div class=\"msg-container\">\
<span class=\"author\" style=\"color: {author_color};\">{author_name}</span>\
<div class=\"msg-body\">\
{attachment}
<div dir=\"auto\" class=\"text\">\
{content}\
</div>\
<div class=\"metadata {with_image_no_caption}\">\
{encryption}\
<span class=\"date date--{direction}\" title=\"{full_time}\">{relative_time}</span>\
<span class=\"spacer\"></span>\
</div>\
</div>\
</div>\
<div>\
</li>",
direction = match message.from_id == DC_CONTACT_ID_SELF {
true => "outgoing",
false => "incoming",
},
avatar = avatar,
author_name = author.name,
author_color = author.color,
attachment = attachment,
content = message.get_text().unwrap_or_else(|| "".to_owned()),
with_image_no_caption = if !has_text && message.get_viewtype() == Viewtype::Image {
"with-image-no-caption"
} else {
""
},
encryption = match message.get_showpadlock() {
true => r#"<div aria-label="Encryption padlock" class="padlock-icon"></div>"#,
false => "",
},
full_time = "Tue, Feb 25, 2020 3:49 PM", // message.get_timestamp() ? // todo
relative_time = "Tue 3:49 PM" // todo
)
// todo link to raw message data
// todo link to message info
}
//TODO tests

View File

@@ -1,5 +1,5 @@
use crate::strum::AsStaticRef;
use mailparse::{MailHeader, MailHeaderMap, MailParseError};
use mailparse::{MailHeader, MailHeaderMap};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[strum(serialize_all = "kebab_case")]
@@ -52,11 +52,11 @@ impl HeaderDef {
}
pub trait HeaderDefMap {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError>;
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
}
impl HeaderDefMap for [MailHeader<'_>] {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError> {
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
self.get_first_value(headerdef.get_headername())
}
}
@@ -79,18 +79,13 @@ mod tests {
let (headers, _) =
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
assert_eq!(
headers
.get_header_value(HeaderDef::AutocryptSetupMessage)
.unwrap(),
headers.get_header_value(HeaderDef::AutocryptSetupMessage),
Some("v99".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::From_).unwrap(),
headers.get_header_value(HeaderDef::From_),
Some("Bob".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::Autocrypt).unwrap(),
None
);
assert_eq!(headers.get_header_value(HeaderDef::Autocrypt), None);
}
}

View File

@@ -188,7 +188,6 @@ struct ImapConfig {
/// True if the server has MOVE capability as defined in
/// https://tools.ietf.org/html/rfc6851
pub can_move: bool,
pub imap_delimiter: char,
}
impl Default for ImapConfig {
@@ -206,7 +205,6 @@ impl Default for ImapConfig {
selected_folder_needs_expunge: false,
can_idle: false,
can_move: false,
imap_delimiter: '.',
}
}
}
@@ -796,7 +794,6 @@ impl Imap {
folder: &str,
uid: u32,
dest_folder: &str,
dest_uid: &mut u32,
) -> ImapActionResult {
task::block_on(async move {
if folder == dest_folder {
@@ -813,10 +810,6 @@ impl Imap {
return imapresult;
}
// we are connected, and the folder is selected
// XXX Rust-Imap provides no target uid on mv, so just set it to 0
*dest_uid = 0;
let set = format!("{}", uid);
let display_folder_id = format!("{}/{}", folder, uid);
@@ -996,10 +989,10 @@ impl Imap {
context: &Context,
message_id: &str,
folder: &str,
uid: &mut u32,
uid: u32,
) -> ImapActionResult {
task::block_on(async move {
if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, *uid) {
if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, uid) {
return imapresult;
}
// we are connected, and the folder is selected
@@ -1021,7 +1014,7 @@ impl Imap {
display_imap_id,
message_id,
);
return ImapActionResult::Failed;
return ImapActionResult::AlreadyDone;
};
let remote_message_id = get_fetch_headers(fetch)
@@ -1036,7 +1029,7 @@ impl Imap {
remote_message_id,
message_id,
);
*uid = 0;
return ImapActionResult::Failed;
}
}
Err(err) => {
@@ -1044,18 +1037,18 @@ impl Imap {
context,
"Cannot delete {} on IMAP: {}", display_imap_id, err
);
*uid = 0;
return ImapActionResult::RetryLater;
}
}
}
// mark the message for deletion
if !self.add_flag_finalized(context, *uid, "\\Deleted").await {
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(
context,
"Cannot mark message {} as \"Deleted\".", display_imap_id
);
ImapActionResult::Failed
ImapActionResult::RetryLater
} else {
emit_event!(
context,
@@ -1074,12 +1067,14 @@ impl Imap {
let folders_configured = context
.sql
.get_raw_config_int(context, "folders_configured");
if folders_configured.unwrap_or_default() >= 3 {
// the "3" here we increase if we have future updates to
// to folder configuration
if folders_configured.unwrap_or_default() >= DC_FOLDERS_CONFIGURED_VERSION {
return Ok(());
}
self.configure_folders(context, create_mvbox)
}
pub fn configure_folders(&self, context: &Context, create_mvbox: bool) -> Result<()> {
task::block_on(async move {
if !self.is_connected().await {
return Err(Error::NoConnection);
@@ -1104,7 +1099,15 @@ impl Imap {
});
info!(context, "sentbox folder is {:?}", sentbox_folder);
let delimiter = self.config.read().await.imap_delimiter;
let mut delimiter = ".";
if let Some(folder) = folders.first() {
if let Some(d) = folder.delimiter() {
if !d.is_empty() {
delimiter = d;
}
}
}
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
let fallback_folder = format!("INBOX{}DeltaChat", delimiter);
let mut mvbox_folder = folders
@@ -1168,9 +1171,11 @@ impl Imap {
Some(sentbox_folder.name()),
)?;
}
context
.sql
.set_raw_config_int(context, "folders_configured", 3)?;
context.sql.set_raw_config_int(
context,
"folders_configured",
DC_FOLDERS_CONFIGURED_VERSION,
)?;
}
info!(context, "FINISHED configuring IMAP-folders.");
Ok(())
@@ -1205,13 +1210,13 @@ impl Imap {
return;
}
if let Err(err) = self.setup_handle_if_needed(context).await {
error!(context, "could not setup imap connection: {:?}", err);
error!(context, "could not setup imap connection: {}", err);
return;
}
if let Err(err) = self.select_folder(context, Some(&folder)).await {
error!(
context,
"Could not select {} for expunging: {:?}", folder, err
"Could not select {} for expunging: {}", folder, err
);
return;
}
@@ -1231,9 +1236,21 @@ impl Imap {
emit_event!(context, Event::ImapFolderEmptied(folder.to_string()));
}
Err(err) => {
error!(context, "expunge failed {}: {:?}", folder, err);
error!(context, "expunge failed {}: {}", folder, err);
}
}
if let Err(err) = crate::sql::execute(
context,
&context.sql,
"UPDATE msgs SET server_folder='',server_uid=0 WHERE server_folder=?",
params![folder],
) {
warn!(
context,
"Failed to reset server_uid and server_folder for deleted messages: {}", err
);
}
});
}
}
@@ -1284,17 +1301,52 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
message::rfc724_mid_exists(context, &rfc724_mid)
{
if old_server_folder.is_empty() && old_server_uid == 0 {
info!(context, "[move] detected bcc-self {}", rfc724_mid,);
context.do_heuristics_moves(server_folder.as_ref(), msg_id);
job_add(
info!(
context,
Action::MarkseenMsgOnImap,
msg_id.to_u32() as i32,
Params::new(),
0,
"[move] detected bcc-self {} as {}/{}", rfc724_mid, server_folder, server_uid
);
let delete_server_after = context.get_config_delete_server_after();
if delete_server_after != Some(0) {
context.do_heuristics_moves(server_folder.as_ref(), msg_id);
job_add(
context,
Action::MarkseenMsgOnImap,
msg_id.to_u32() as i32,
Params::new(),
0,
);
}
} else if old_server_folder != server_folder {
info!(context, "[move] detected moved message {}", rfc724_mid,);
info!(
context,
"[move] detected message {} moved by other device from {}/{} to {}/{}",
rfc724_mid,
old_server_folder,
old_server_uid,
server_folder,
server_uid
);
} else if old_server_uid == 0 {
info!(
context,
"[move] detected message {} moved by us from {}/{} to {}/{}",
rfc724_mid,
old_server_folder,
old_server_uid,
server_folder,
server_uid
);
} else if old_server_uid != server_uid {
warn!(
context,
"UID for message {} in folder {} changed from {} to {}",
rfc724_mid,
server_folder,
old_server_uid,
server_uid
);
}
if old_server_folder != server_folder || old_server_uid != server_uid {
@@ -1316,30 +1368,27 @@ fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>>
}
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId)? {
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId) {
Ok(crate::mimeparser::parse_message_id(&message_id)?)
} else {
Err(Error::Other("prefetch: No message ID found".to_string()))
}
}
fn prefetch_is_reply_to_chat_message(
context: &Context,
headers: &[mailparse::MailHeader],
) -> Result<bool> {
if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo)? {
fn prefetch_is_reply_to_chat_message(context: &Context, headers: &[mailparse::MailHeader]) -> bool {
if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo) {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
return true;
}
}
if let Some(value) = headers.get_header_value(HeaderDef::References)? {
if let Some(value) = headers.get_header_value(HeaderDef::References) {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
return true;
}
}
Ok(false)
false
}
fn prefetch_should_download(
@@ -1347,16 +1396,16 @@ fn prefetch_should_download(
headers: &[mailparse::MailHeader],
show_emails: ShowEmails,
) -> Result<bool> {
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion)?.is_some();
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers)?;
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers);
// Autocrypt Setup Message should be shown even if it is from non-chat client.
let is_autocrypt_setup_message = headers
.get_header_value(HeaderDef::AutocryptSetupMessage)?
.get_header_value(HeaderDef::AutocryptSetupMessage)
.is_some();
let from_field = headers
.get_header_value(HeaderDef::From_)?
.get_header_value(HeaderDef::From_)
.unwrap_or_default();
let (_contact_id, blocked_contact, origin) = from_field_to_contact_id(context, &from_field)?;

View File

@@ -756,7 +756,6 @@ mod tests {
assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
assert!(msg.contains("Passphrase-Begin: he\n"));
assert!(msg.contains("==\n"));
assert!(msg.contains("-----END PGP MESSAGE-----\n"));
}

View File

@@ -80,10 +80,14 @@ pub enum Action {
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
Housekeeping = 105, // low priority ...
EmptyServer = 107,
DeleteMsgOnImap = 110,
MarkseenMdnOnImap = 120,
OldDeleteMsgOnImap = 110,
MarkseenMsgOnImap = 130,
// Moving message is prioritized lower than deletion so we don't
// bother moving message if it is already scheduled for deletion.
MoveMsg = 200,
DeleteMsgOnImap = 210,
ConfigureImap = 900,
ImexImap = 910, // ... high priority
@@ -108,9 +112,9 @@ impl From<Action> for Thread {
Unknown => Thread::Unknown,
Housekeeping => Thread::Imap,
OldDeleteMsgOnImap => Thread::Imap,
DeleteMsgOnImap => Thread::Imap,
EmptyServer => Thread::Imap,
MarkseenMdnOnImap => Thread::Imap,
MarkseenMsgOnImap => Thread::Imap,
MoveMsg => Thread::Imap,
ConfigureImap => Thread::Imap,
@@ -413,18 +417,12 @@ impl Job {
if let Some(dest_folder) = dest_folder {
let server_folder = msg.server_folder.as_ref().unwrap();
let mut dest_uid = 0;
match imap_inbox.mv(
context,
server_folder,
msg.server_uid,
&dest_folder,
&mut dest_uid,
) {
match imap_inbox.mv(context, server_folder, msg.server_uid, &dest_folder) {
ImapActionResult::RetryLater => Status::RetryLater,
ImapActionResult::Success => {
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, dest_uid);
// XXX Rust-Imap provides no target uid on mv, so just set it to 0
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, 0);
Status::Finished(Ok(()))
}
ImapActionResult::Failed => {
@@ -437,14 +435,29 @@ impl Job {
}
}
/// Deletes a message on the server.
///
/// foreign_id is a MsgId pointing to a message in the trash chat
/// or a hidden message.
///
/// This job removes the database record. If there are no more
/// records pointing to the same message on the server, the job
/// also removes the message on the server.
#[allow(non_snake_case)]
fn DeleteMsgOnImap(&mut self, context: &Context) -> Status {
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
let mut msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)));
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)));
if !msg.rfc724_mid.is_empty() {
if message::rfc724_mid_cnt(context, &msg.rfc724_mid) > 1 {
let cnt = message::rfc724_mid_cnt(context, &msg.rfc724_mid);
info!(
context,
"Running delete job for message {} which has {} entries in the database",
&msg.rfc724_mid,
cnt
);
if cnt > 1 {
info!(
context,
"The message is deleted from the server when all parts are deleted.",
@@ -454,13 +467,47 @@ impl Job {
we delete the message from the server */
let mid = msg.rfc724_mid;
let server_folder = msg.server_folder.as_ref().unwrap();
let res = imap_inbox.delete_msg(context, &mid, server_folder, &mut msg.server_uid);
if res == ImapActionResult::RetryLater {
// XXX RetryLater is converted to RetryNow here
return Status::RetryNow;
let res = if msg.server_uid == 0 {
// Message is already deleted on IMAP server.
ImapActionResult::AlreadyDone
} else {
imap_inbox.delete_msg(context, &mid, server_folder, msg.server_uid)
};
match res {
ImapActionResult::AlreadyDone | ImapActionResult::Success => {}
ImapActionResult::RetryLater | ImapActionResult::Failed => {
// If job has failed, for example due to some
// IMAP bug, we postpone it instead of failing
// immediately. This will prevent adding it
// immediately again if user has enabled
// automatic message deletion. Without this,
// we might waste a lot of traffic constantly
// retrying message deletion.
return Status::RetryLater;
}
}
}
Message::delete_from_db(context, msg.id);
if msg.chat_id.is_trash() || msg.hidden {
// Messages are stored in trash chat only to keep
// their server UID and Message-ID. Once message is
// deleted from the server, database record can be
// removed as well.
//
// Hidden messages are similar to trashed, but are
// related to some chat. We also delete their
// database records.
job_try!(msg.id.delete_from_db(context))
} else {
// Remove server UID from the database record.
//
// We have either just removed the message from the
// server, in which case UID is not valid anymore, or
// we have more refernces to the same server UID, so
// we remove UID to reduce the number of messages
// pointing to the corresponding UID. Once the counter
// reaches zero, we will remove the message.
job_try!(msg.id.unlink(context));
}
Status::Finished(Ok(()))
} else {
/* eg. device messages have no Message-ID */
@@ -512,43 +559,6 @@ impl Job {
}
}
}
#[allow(non_snake_case)]
fn MarkseenMdnOnImap(&mut self, context: &Context) -> Status {
let folder = self
.param
.get(Param::ServerFolder)
.unwrap_or_default()
.to_string();
let uid = self.param.get_int(Param::ServerUid).unwrap_or_default() as u32;
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
if imap_inbox.set_seen(context, &folder, uid) == ImapActionResult::RetryLater {
return Status::RetryLater;
}
if self.param.get_bool(Param::AlsoMove).unwrap_or_default() {
if let Err(err) = imap_inbox.ensure_configured_folders(context, true) {
warn!(context, "configuring folders failed: {:?}", err);
return Status::RetryLater;
}
let dest_folder = context
.sql
.get_raw_config(context, "configured_mvbox_folder");
if let Some(dest_folder) = dest_folder {
let mut dest_uid = 0;
if ImapActionResult::RetryLater
== imap_inbox.mv(context, &folder, uid, &dest_folder, &mut dest_uid)
{
Status::RetryLater
} else {
Status::Finished(Ok(()))
}
} else {
Status::Finished(Err(format_err!("MVBOX is not configured")))
}
} else {
Status::Finished(Ok(()))
}
}
}
/* delete all pending jobs with the given action */
@@ -821,7 +831,11 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
let lowercase_from = from.to_lowercase();
// Send BCC to self if it is enabled and we are not going to
// delete it immediately.
if context.get_config_bool(Config::BccSelf)
&& context.get_config_delete_server_after() != Some(0)
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
@@ -897,6 +911,41 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
Ok(())
}
fn add_imap_deletion_jobs(context: &Context) -> sql::Result<()> {
if let Some(delete_server_after) = context.get_config_delete_server_after() {
let threshold_timestamp = time() - delete_server_after;
// Select all expired messages which don't have a
// corresponding message deletion job yet.
let msg_ids = context.sql.query_map(
"SELECT id FROM msgs \
WHERE timestamp < ? \
AND server_uid != 0 \
AND NOT EXISTS (SELECT 1 FROM jobs WHERE foreign_id = msgs.id \
AND action = ?)",
params![threshold_timestamp, Action::DeleteMsgOnImap],
|row| row.get::<_, MsgId>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)?;
// Schedule IMAP deletion for expired messages.
for msg_id in msg_ids {
job_add(
context,
Action::DeleteMsgOnImap,
msg_id.to_u32() as i32,
Params::new(),
0,
)
}
}
Ok(())
}
pub fn perform_inbox_jobs(context: &Context) {
info!(context, "dc_perform_inbox_jobs starting.",);
@@ -904,6 +953,9 @@ pub fn perform_inbox_jobs(context: &Context) {
*context.probe_imap_network.write().unwrap() = false;
*context.perform_inbox_jobs_needed.write().unwrap() = false;
if let Err(err) = add_imap_deletion_jobs(context) {
warn!(context, "Can't add IMAP message deletion jobs: {}", err);
}
job_perform(context, Thread::Imap, probe_imap_network);
info!(context, "dc_perform_inbox_jobs ended.",);
}
@@ -1045,9 +1097,9 @@ fn perform_job_action(context: &Context, mut job: &mut Job, thread: Thread, trie
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
Action::SendMsgToSmtp => job.SendMsgToSmtp(context),
Action::EmptyServer => job.EmptyServer(context),
Action::OldDeleteMsgOnImap => job.DeleteMsgOnImap(context),
Action::DeleteMsgOnImap => job.DeleteMsgOnImap(context),
Action::MarkseenMsgOnImap => job.MarkseenMsgOnImap(context),
Action::MarkseenMdnOnImap => job.MarkseenMdnOnImap(context),
Action::MoveMsg => job.MoveMsg(context),
Action::SendMdn => job.SendMdn(context),
Action::ConfigureImap => JobConfigureImap(context),

View File

@@ -36,6 +36,7 @@ pub mod constants;
pub mod contact;
pub mod context;
mod e2ee;
pub mod export_chat;
mod imap;
pub mod imex;
#[macro_use]

View File

@@ -85,6 +85,55 @@ impl MsgId {
self.0 == DC_MSG_ID_DAYMARKER
}
/// Put message into trash chat and delete message text.
///
/// It means the message is deleted locally, but not on the server
/// yet.
pub fn trash(self, context: &Context) -> crate::sql::Result<()> {
let chat_id = ChatId::new(DC_CHAT_ID_TRASH);
sql::execute(
context,
&context.sql,
"UPDATE msgs SET chat_id=?, txt='', txt_raw='' WHERE id=?",
params![chat_id, self],
)
}
/// Deletes a message and corresponding MDNs from the database.
pub fn delete_from_db(self, context: &Context) -> crate::sql::Result<()> {
// We don't use transactions yet, so remove MDNs first to make
// sure they are not left while the message is deleted.
sql::execute(
context,
&context.sql,
"DELETE FROM msgs_mdns WHERE msg_id=?;",
params![self],
)?;
sql::execute(
context,
&context.sql,
"DELETE FROM msgs WHERE id=?;",
params![self],
)?;
Ok(())
}
/// Removes IMAP server UID and folder from the database record.
///
/// It is used to avoid trying to remove the message from the
/// server multiple times when there are multiple message records
/// pointing to the same server UID.
pub(crate) fn unlink(self, context: &Context) -> sql::Result<()> {
sql::execute(
context,
&context.sql,
"UPDATE msgs \
SET server_folder='', server_uid=0 \
WHERE id=?",
params![self],
)
}
/// Bad evil escape hatch.
///
/// Avoid using this, eventually types should be cleaned up enough
@@ -305,25 +354,6 @@ impl Message {
.map_err(Into::into)
}
pub fn delete_from_db(context: &Context, msg_id: MsgId) {
if let Ok(msg) = Message::load_from_db(context, msg_id) {
sql::execute(
context,
&context.sql,
"DELETE FROM msgs WHERE id=?;",
params![msg.id],
)
.ok();
sql::execute(
context,
&context.sql,
"DELETE FROM msgs_mdns WHERE msg_id=?;",
params![msg.id],
)
.ok();
}
}
pub fn get_filemime(&self) -> Option<String> {
if let Some(m) = self.param.get(Param::MimeType) {
return Some(m.to_string());
@@ -951,13 +981,15 @@ pub fn get_mime_headers(context: &Context, msg_id: MsgId) -> Option<String> {
}
pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
for msg_id in msg_ids.iter() {
for msg_id in msg_ids {
if let Ok(msg) = Message::load_from_db(context, *msg_id) {
if msg.location_id > 0 {
delete_poi_location(context, msg.location_id);
}
}
update_msg_chat_id(context, *msg_id, ChatId::new(DC_CHAT_ID_TRASH));
if let Err(err) = msg_id.trash(context) {
error!(context, "Unable to trash message {}: {}", msg_id, err);
}
job_add(
context,
Action::DeleteMsgOnImap,
@@ -977,16 +1009,6 @@ pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
};
}
fn update_msg_chat_id(context: &Context, msg_id: MsgId, chat_id: ChatId) -> bool {
sql::execute(
context,
&context.sql,
"UPDATE msgs SET chat_id=? WHERE id=?;",
params![chat_id, msg_id],
)
.is_ok()
}
fn delete_poi_location(context: &Context, location_id: u32) -> bool {
sql::execute(
context,
@@ -1347,10 +1369,55 @@ pub fn get_deaddrop_msg_cnt(context: &Context) -> usize {
}
}
pub fn estimate_deletion_cnt(
context: &Context,
from_server: bool,
seconds: i64,
) -> Result<usize, Error> {
let self_chat_id = chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.unwrap_or_default()
.0;
let threshold_timestamp = time() - seconds;
let cnt: isize = if from_server {
context.sql.query_row(
"SELECT COUNT(*)
FROM msgs m
WHERE m.id > ?
AND timestamp < ?
AND chat_id != ?
AND server_uid != 0;",
params![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id],
|row| row.get(0),
)?
} else {
context.sql.query_row(
"SELECT COUNT(*)
FROM msgs m
WHERE m.id > ?
AND timestamp < ?
AND chat_id != ?
AND chat_id != ? AND hidden = 0;",
params![
DC_MSG_ID_LAST_SPECIAL,
threshold_timestamp,
self_chat_id,
ChatId::new(DC_CHAT_ID_TRASH)
],
|row| row.get(0),
)?
};
Ok(cnt as usize)
}
/// Counts number of database records pointing to specified
/// Message-ID.
///
/// Unlinked messages are excluded.
pub fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 {
// check the number of messages with the same rfc724_mid
match context.sql.query_row(
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=?;",
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0",
&[rfc724_mid],
|row| row.get(0),
) {
@@ -1391,7 +1458,8 @@ pub fn update_server_uid(
server_uid: u32,
) {
match context.sql.execute(
"UPDATE msgs SET server_folder=?, server_uid=? WHERE rfc724_mid=?;",
"UPDATE msgs SET server_folder=?, server_uid=? \
WHERE rfc724_mid=?",
params![server_folder.as_ref(), server_uid, rfc724_mid],
) {
Ok(_) => {}

View File

@@ -368,7 +368,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
self.from_addr.clone(),
);
let mut to = Vec::with_capacity(self.recipients.len());
let mut to = Vec::new();
for (name, addr) in self.recipients.iter() {
if name.is_empty() {
to.push(Address::new_mailbox(addr.clone()));
@@ -380,6 +380,10 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
if to.is_empty() {
to.push(from.clone());
}
if !self.references.is_empty() {
unprotected_headers.push(Header::new("References".into(), self.references.clone()));
}

View File

@@ -7,7 +7,6 @@ use mailparse::{DispositionType, MailAddr, MailHeaderMap};
use crate::aheader::Aheader;
use crate::bail;
use crate::blob::BlobObject;
use crate::config::Config;
use crate::constants::Viewtype;
use crate::contact::*;
use crate::context::Context;
@@ -17,7 +16,6 @@ use crate::e2ee;
use crate::error::Result;
use crate::events::Event;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{job_add, Action};
use crate::location;
use crate::message;
use crate::param::*;
@@ -85,7 +83,7 @@ impl MimeMessage {
let message_time = mail
.headers
.get_header_value(HeaderDef::Date)?
.get_header_value(HeaderDef::Date)
.and_then(|v| mailparse::dateparse(&v).ok())
.unwrap_or_default();
@@ -111,8 +109,7 @@ impl MimeMessage {
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
let gossip_headers =
decrypted_mail.headers.get_all_values("Autocrypt-Gossip")?;
let gossip_headers = decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossipped_addr =
update_gossip_peerstates(context, message_time, &mail, gossip_headers)?;
@@ -545,6 +542,16 @@ impl MimeMessage {
if let Some(report) = self.process_report(context, mail)? {
self.reports.push(report);
}
// Add MDN part so we can track it, avoid
// downloading the message again and
// delete if automatic message deletion is
// enabled.
let mut part = Part::default();
part.typ = Viewtype::Unknown;
self.parts.push(part);
any_part_added = true;
} else {
/* eg. `report-type=delivery-status`;
maybe we should show them as a little error icon */
@@ -746,16 +753,13 @@ impl MimeMessage {
fn merge_headers(headers: &mut HashMap<String, String>, fields: &[mailparse::MailHeader<'_>]) {
for field in fields {
if let Ok(key) = field.get_key() {
// lowercasing all headers is technically not correct, but makes things work better
let key = key.to_lowercase();
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
// lowercasing all headers is technically not correct, but makes things work better
let key = field.get_key().to_lowercase();
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
is_known(&key) || key.starts_with("chat-")
{
if let Ok(value) = field.get_value() {
headers.insert(key, value);
}
}
{
let value = field.get_value();
headers.insert(key.to_string(), value);
}
}
}
@@ -770,21 +774,13 @@ impl MimeMessage {
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
// must be present
if let Some(_disposition) = report_fields
.get_header_value(HeaderDef::Disposition)
.ok()
.flatten()
{
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
if let Some(original_message_id) = report_fields
.get_header_value(HeaderDef::OriginalMessageId)
.ok()
.flatten()
.and_then(|v| parse_message_id(&v).ok())
{
let additional_message_ids = report_fields
.get_header_value(HeaderDef::AdditionalMessageIds)
.ok()
.flatten()
.map_or_else(Vec::new, |v| {
v.split(' ')
.filter_map(|s| parse_message_id(s).ok())
@@ -800,26 +796,18 @@ impl MimeMessage {
warn!(
context,
"ignoring unknown disposition-notification, Message-Id: {:?}",
report_fields.get_header_value(HeaderDef::MessageId).ok()
report_fields.get_header_value(HeaderDef::MessageId)
);
Ok(None)
}
/// Handle reports (only MDNs for now)
pub fn handle_reports(
&self,
context: &Context,
from_id: u32,
sent_timestamp: i64,
server_folder: impl AsRef<str>,
server_uid: u32,
) {
pub fn handle_reports(&self, context: &Context, from_id: u32, sent_timestamp: i64) {
if self.reports.is_empty() {
return;
}
let mut mdn_recognized = false;
for report in &self.reports {
for original_message_id in
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
@@ -828,20 +816,9 @@ impl MimeMessage {
message::mdn_from_ext(context, from_id, original_message_id, sent_timestamp)
{
context.call_cb(Event::MsgRead { chat_id, msg_id });
mdn_recognized = true;
}
}
}
if self.has_chat_version() || mdn_recognized {
let mut param = Params::new();
param.set(Param::ServerFolder, server_folder.as_ref());
param.set_int(Param::ServerUid, server_uid as i32);
if self.has_chat_version() && context.get_config_bool(Config::MvboxMove) {
param.set_int(Param::AlsoMove, 1);
}
job_add(context, Action::MarkseenMdnOnImap, 0, param, 0);
}
}
}
@@ -860,14 +837,9 @@ fn update_gossip_peerstates(
if let Ok(ref header) = gossip_header {
if recipients.is_none() {
recipients = Some(get_recipients(mail.headers.iter().filter_map(|v| {
let key = v.get_key();
let value = v.get_value();
if key.is_err() || value.is_err() {
return None;
}
Some((v.get_key().unwrap(), v.get_value().unwrap()))
})));
recipients = Some(get_recipients(
mail.headers.iter().map(|v| (v.get_key(), v.get_value())),
));
}
if recipients
@@ -915,13 +887,8 @@ pub(crate) fn parse_message_id(value: &str) -> crate::error::Result<String> {
let ids = mailparse::msgidparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
if ids.len() == 1 {
let id = &ids[0];
if id.starts_with('<') && id.ends_with('>') {
Ok(id.chars().skip(1).take(id.len() - 2).collect())
} else {
bail!("message-ID {} is not enclosed in < and >", value);
}
if let Some(id) = ids.first() {
Ok(id.to_string())
} else {
bail!("could not parse message_id: {}", value);
}
@@ -987,15 +954,12 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
}
fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
if let Ok(ct) = mail.get_content_disposition() {
return ct.disposition == DispositionType::Attachment
&& ct
.params
.iter()
.any(|(key, _value)| key.starts_with("filename"));
}
false
let ct = mail.get_content_disposition();
ct.disposition == DispositionType::Attachment
&& ct
.params
.iter()
.any(|(key, _value)| key.starts_with("filename"))
}
/// Tries to get attachment filename.
@@ -1010,7 +974,7 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<Option<String
// or `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...`
// or `Content-Disposition: ... filename=...`
let ct = mail.get_content_disposition()?;
let ct = mail.get_content_disposition();
let desired_filename: Option<String> = ct
.params
@@ -1369,7 +1333,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
Some("Chat: Message opened".to_string())
);
assert_eq!(message.parts.len(), 0);
assert_eq!(message.parts.len(), 1);
assert_eq!(message.reports.len(), 1);
}
@@ -1447,7 +1411,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
Some("Chat: Message opened".to_string())
);
assert_eq!(message.parts.len(), 0);
assert_eq!(message.parts.len(), 2);
assert_eq!(message.reports.len(), 2);
}
@@ -1492,7 +1456,7 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
Some("Chat: Message opened".to_string())
);
assert_eq!(message.parts.len(), 0);
assert_eq!(message.parts.len(), 1);
assert_eq!(message.reports.len(), 1);
assert_eq!(message.reports[0].original_message_id, "foo@example.org");
assert_eq!(

View File

@@ -88,12 +88,6 @@ pub enum Param {
/// For Jobs
SetLongitude = b'n',
/// For Jobs
ServerFolder = b'Z',
/// For Jobs
ServerUid = b'z',
/// For Jobs
AlsoMove = b'M',

View File

@@ -153,8 +153,8 @@ pub(crate) fn create_keypair(
keygen_type: KeyGenType,
) -> std::result::Result<KeyPair, PgpKeygenError> {
let (secret_key_type, public_key_type) = match keygen_type {
KeyGenType::Rsa2048 | KeyGenType::Default => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Ed25519 => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
};
let user_id = format!("<{}>", addr);
@@ -394,7 +394,6 @@ mod tests {
}
#[test]
#[ignore] // is too expensive
fn test_create_keypair() {
let keypair0 = create_keypair(
EmailAddress::new("foo@bar.de").unwrap(),

View File

@@ -770,6 +770,38 @@ pub(crate) fn handle_securejoin_handshake(
}
}
/// observe_securejoin_on_other_device() must be called when a self-sent securejoin message is seen.
/// currently, the message is only ignored, in the future,
/// we may mark peers as verified accross devices:
///
/// in a multi-device-setup, there may be other devices that "see" the handshake messages.
/// if the seen messages seen are self-sent messages encrypted+signed correctly with our key,
/// we can make some conclusions of it:
///
/// - if we see the self-sent-message vg-member-added/vc-contact-confirm,
/// we know that we're an inviter-observer.
/// the inviting device has marked a peer as verified on vg-request-with-auth/vc-request-with-auth
/// before sending vg-member-added/vc-contact-confirm - so, if we observe vg-member-added/vc-contact-confirm,
/// we can mark the peer as verified as well.
///
/// - if we see the self-sent-message vg-member-added-received
/// we know that we're an joiner-observer.
/// the joining device has marked the peer as verified on vg-member-added/vc-contact-confirm
/// before sending vg-member-added-received - so, if we observe vg-member-added-received,
/// we can mark the peer as verified as well.
///
/// to make this work, (a) some messages must not be deleted,
/// (b) we need a vc-contact-confirm-received message if bcc_self is set,
/// (c) we should make sure, we do not only rely on the unencrypted To:-header for identifying the peer
/// (in handle_securejoin_handshake() we have the oob information for that)
pub(crate) fn observe_securejoin_on_other_device(
_context: &Context,
_mime_message: &MimeMessage,
_contact_id: u32,
) -> Result<HandshakeMessage, HandshakeError> {
Ok(HandshakeMessage::Ignore)
}
fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id);
let contact = Contact::get_by_id(context, contact_id);

View File

@@ -8,7 +8,7 @@ use rusqlite::{Connection, OpenFlags, Statement, NO_PARAMS};
use thread_local_object::ThreadLocal;
use crate::chat::{update_device_icon, update_saved_messages_icon};
use crate::constants::ShowEmails;
use crate::constants::{ShowEmails, DC_CHAT_ID_TRASH};
use crate::context::Context;
use crate::dc_tools::*;
use crate::param::*;
@@ -1053,6 +1053,18 @@ pub fn get_rowid2_with_conn(
}
}
/// Removes from the database locally deleted messages that also don't
/// have a server UID.
fn prune_tombstones(context: &Context) -> Result<()> {
context.sql.execute(
"DELETE FROM msgs \
WHERE (chat_id = ? OR hidden) \
AND server_uid = 0",
params![DC_CHAT_ID_TRASH],
)?;
Ok(())
}
pub fn housekeeping(context: &Context) {
let mut files_in_use = HashSet::new();
let mut unreferenced_count = 0;
@@ -1165,6 +1177,13 @@ pub fn housekeeping(context: &Context) {
}
}
if let Err(err) = prune_tombstones(context) {
warn!(
context,
"Houskeeping: Cannot prune message tombstones: {}", err
);
}
info!(context, "Housekeeping done.",);
}

View File

@@ -1 +1 @@
xsBNBF425W8BCADLIbltPzG1vk/V2ov2+eBeJJJnRu1kJHdo6e3oNB+HTIxVde5+7Uq8tTEDZB1O7m9NBUFrXr7UYQsA/86G2jmsyWKTzIu1O/t5kdcNDqsNcTVZAhBu2ixYsYVc3ws6kJONjpXLtD2u3P7vEXU3INiOb2JrBQDT8/ubEm1xas/UirYnP5DMaH068IHRdVEYs9ULFaD5scw1m/94buXYZ1CRt/2hT8iRrtBi6ki8kArnhsZC2Xr0+jRQNMUnG5k7Bwi6saCqVmd7IlqSM6MbfYank30Gi/UyDmyIrOk7daTg6WIqgiVOTHav65EK/aUvvjlr+awM+C+u35rQytzyTitZABEBAAHNEzxhbGljZUBleGFtcGxlLmNvbT7CwIkEEAEIADMCGQEFAl425ZQCGwMECwkIBwYVCAkKCwIDFgIBFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HiZQf+PLDxzWchkHAdQFbxxtoXj66aiknofjlRWHDWvUG4nULZ15tjDjnv3z22Meldr8kSV4r1+ejhLFHou9gTzAYk7eAxiybDd8AJOdK+ZgK/Nn7xjdO+HTZLhNdi+R7EektDyf8WDNktEaS8pZc74VKu4984ESi4PoqVxqGHRiSisH4cw4b2pQYxp32BkIdil7sWnqRUEoCpMoKdw2h0N7/lm+rS7/JR9cdjXaVzy1dYTqAVsTL1FTGy4osOKGOyQbkP+Cm6uNq7kC/Bt+fefsb+c2JycmI1uwdvnG7PoFslKv3lRnfkNSmrcIYlJHUl5z0yAgliophr5fqMfzQpO4zMc87ATQReNuVvAQgAuNjE1i+g4v25UNDPIMgXODU4WztE30074gQs5sZa0DQnDUMsdWc2g1o060YZDojMYJQAtBjlW1Dz8FEE7WsLNohGtRyUWmIgNxE5CpodjpwIZ0MdO4Aji0YM+g+WsOSS8kiHMs+dMFfQJuNKjujGFaMIciSaMMrUmPtzkQ/o8NEJs2Aftw90fpVR+M7Mue3++rcEX09ntbgqkgm8SV6OIrOY2kfILudtybocgYkCTeNVqz5VFXuxrnT4ceyFQ64JkwsZxb+X/pCm4V5Q2TbKRwtdonU8HfAz0nAd5tsNeGmf/dPLOKBCxlNEme399YmzWrT+kJBp7CIH5jlWQKyuLwARAQABwsB2BBgBCAAgBQJeNuWUAhsMFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HrEgf/Xu8eRPPdskwtyd98y64teidBpkHuIjuZKJpNyy2HhdGXQwYbNIzwINg0EJ+u2nkreNF/h2Lu+/saqI8Dai02dpYXjvxJIlCgP2os7sNhVaZSaS4XmmJjkHCfZuIKblZypKDJVc5AceZxrtvUbgG+94+H3zeRWVAA30S5ep6YPvxigvhmQah/sdzY7708/jd9uXcCbkP47PBaXCpuPiYLb3t7z8mOteJb7LOZUmSI1efiLDLTGj7ofkdDfA7E6/nF/1+nq+UIDWqljwiUzeNIJsFlZRa/9/uDEjcQbaDe9/knBs7k9pEDZX5u8SSwSED75L+OvRpFWenp4SSKvd2BUw==
mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4MCyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDrRuI8A/8tEEXAA7g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeIeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg==

View File

@@ -1 +1 @@
xcLYBF425W8BCADLIbltPzG1vk/V2ov2+eBeJJJnRu1kJHdo6e3oNB+HTIxVde5+7Uq8tTEDZB1O7m9NBUFrXr7UYQsA/86G2jmsyWKTzIu1O/t5kdcNDqsNcTVZAhBu2ixYsYVc3ws6kJONjpXLtD2u3P7vEXU3INiOb2JrBQDT8/ubEm1xas/UirYnP5DMaH068IHRdVEYs9ULFaD5scw1m/94buXYZ1CRt/2hT8iRrtBi6ki8kArnhsZC2Xr0+jRQNMUnG5k7Bwi6saCqVmd7IlqSM6MbfYank30Gi/UyDmyIrOk7daTg6WIqgiVOTHav65EK/aUvvjlr+awM+C+u35rQytzyTitZABEBAAEAB/oDQFnwdrd7+jza5nGhFWTS/PDe+FKqbK8AneXx9ouepcoFQCr+Gxw8IwZS0JJrhgOADxp59n1FdvwvGukaXXnY2yxZw0dlMj2XN49ipR51y58X+qF6tMFK9iR1VRif6lqCRIr/RLZMCzuFZhkjNcJhnUTNA7p8qgYX+FaKHzSOaVat/v0kIUHUcZDkREWPUESYDmc1Nv6FXhB0WBiTsBglF+fq5Rm7UWPSmA59Cr7BrW8DctbzTh0+6bkzum2xdOcZ59nuTZa+IKcReI1+kVne5JPNFNJ2tP2f9GSSlL7u+NBtx3zRxZgAotXcJK9cVNIWtegqf+2hoLvm7m2CkWKRBADglpC7TpjV+8wJH+KuyGQ7jepqzf5EHwMrK2i6lPnnmoi0nkKvkklvtdcC7FoFGtLCDJ7vwlUdeN+itDxPlP8bbbUabcy0lLuzyGOVt5NwYXgIuPicpdt2ZTJgvChd9oWi1DG8pVpm+EMJZPyYVEpvDGl6q95oktrytbqjASZbBQQA54roJnwBcptLMTrttDrglULX7ciSKY5HXN1c0rqZn1dTKB1nPYB26hNbu6lZ8ixSOyZm3KwpeDUNW7A3hyzXOfoGFPaddH6WMSFFsGGC/orRVxnuPZLr3UJ3uFX7J0JOav90n/6A4YmS7uImRAG4/vTrAbEfmlBl5msHVUaYh0UD/jSX22JLenO1o8pNU04JQl3lQ4mWY6MvgTyCvpchTzDDva+wdOBTUeVUmb/KqYkYBq98tXl1VnGnNpeEymUISSi60RjaXDhbg7a3ELV0yvvWcBN9zreyyINuCU5OmNefPRvPt4Co12KtIxPACByFNTevzPKbrXd1cyhHOxAuqfzLRbDNEzxhbGljZUBleGFtcGxlLmNvbT7CwIkEEAEIADMCGQEFAl425ZQCGwMECwkIBwYVCAkKCwIDFgIBFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HiZQf+PLDxzWchkHAdQFbxxtoXj66aiknofjlRWHDWvUG4nULZ15tjDjnv3z22Meldr8kSV4r1+ejhLFHou9gTzAYk7eAxiybDd8AJOdK+ZgK/Nn7xjdO+HTZLhNdi+R7EektDyf8WDNktEaS8pZc74VKu4984ESi4PoqVxqGHRiSisH4cw4b2pQYxp32BkIdil7sWnqRUEoCpMoKdw2h0N7/lm+rS7/JR9cdjXaVzy1dYTqAVsTL1FTGy4osOKGOyQbkP+Cm6uNq7kC/Bt+fefsb+c2JycmI1uwdvnG7PoFslKv3lRnfkNSmrcIYlJHUl5z0yAgliophr5fqMfzQpO4zMc8fC2AReNuVvAQgAuNjE1i+g4v25UNDPIMgXODU4WztE30074gQs5sZa0DQnDUMsdWc2g1o060YZDojMYJQAtBjlW1Dz8FEE7WsLNohGtRyUWmIgNxE5CpodjpwIZ0MdO4Aji0YM+g+WsOSS8kiHMs+dMFfQJuNKjujGFaMIciSaMMrUmPtzkQ/o8NEJs2Aftw90fpVR+M7Mue3++rcEX09ntbgqkgm8SV6OIrOY2kfILudtybocgYkCTeNVqz5VFXuxrnT4ceyFQ64JkwsZxb+X/pCm4V5Q2TbKRwtdonU8HfAz0nAd5tsNeGmf/dPLOKBCxlNEme399YmzWrT+kJBp7CIH5jlWQKyuLwARAQABAAf/YmpfWp5fLZvjJ8kVDqIZ4r5LNB+5Sp7nbC3G7lPblBDAXgpOyG9ckdDcbguTWa6yChWizkCXFOhkCKZKVlHw1Wb3JoSB5CFsf4U29pMZe41N2BTeoohV5Fg2nojgNWxtZHwDJ6VsTonidGH9l1sN5AU6gPNF+QZ07MKsRCbRYi0yMgX064gwZXRtkm8AECz8ay1wDzoBy14ALe9aDClafVwfxdYUcxDBqtvjLhGeTWX5lMMAQ1Ix8D0Gp4r0Zvtl+oxlTSZFAt9m6sbRBbJf4LJjRQh07aWF2gUOiyIyz7YymYdwsyFnCPn2Aj84uRdqYCekAUfzBeNTBukUQq1DYQQA3BeH38pnr34m0UyD/tCrTvrX60MOJVvFuaTQw+IgY4XmT9UiiiqYMaoLfzxeevdMCQ9EtMdXUTjI27/II3dR5Obg6J0QTybj78IKPbH8Vdlg0etllRjC3bV/M4a5UcXPKG6W5CvB0UJg7eqn/8wUqwiL9x+hZoLy+nU5rCAjzZEEANcBK8Vy9eBxkKmEfH/mChDSKE82ua0xdQuZiTvvGedUYG3ucH4rAlkZaZcZrtJTod1BKhAhDBrjxk/yLCjK3z5JDsacdDGGfaqga3zdPBJubWE7f4mg6uYVs04Uf90YVY7t0LEQAh7i9QYiIqUOJDy3L8y3+bNgNz2r1p8pFd+/A/0YoYE0YDgABbLKmBQFoWjF3Op7P+k6Z4ENK4Me1fkNSAU451QX7ZduI7i3pGTM06bXG2umhTI1lg48ZveMRk1vBezHU+ThnciEkuhYafnq7NRdkEtI20MyFmN7dZF8LQ/joYKsJbeSG5svj8f1ue2eHkiIIlTtDqVUTizDU3ddlzUZwsB2BBgBCAAgBQJeNuWUAhsMFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HrEgf/Xu8eRPPdskwtyd98y64teidBpkHuIjuZKJpNyy2HhdGXQwYbNIzwINg0EJ+u2nkreNF/h2Lu+/saqI8Dai02dpYXjvxJIlCgP2os7sNhVaZSaS4XmmJjkHCfZuIKblZypKDJVc5AceZxrtvUbgG+94+H3zeRWVAA30S5ep6YPvxigvhmQah/sdzY7708/jd9uXcCbkP47PBaXCpuPiYLb3t7z8mOteJb7LOZUmSI1efiLDLTGj7ofkdDfA7E6/nF/1+nq+UIDWqljwiUzeNIJsFlZRa/9/uDEjcQbaDe9/knBs7k9pEDZX5u8SSwSED75L+OvRpFWenp4SSKvd2BUw==
lFgEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5AAAQDMpCY4sD5/DUR0jRjGC5WstwShz1q+5Vofo5mY9+XRXRA3tBlBbGljZSA8YWxpY2VAZXhhbXBsZS5vcmc+iJAEExYIADgWIQQub6LLI7Uy1yhjS1hksI9hqe2UQwUCXlh13QIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBksI9hqe2UQxN6AP4uDAsgzE7I1qg2aEMF/wPxqEztv1dMCj4YyScv/JtqTAD5AYzpxjrcdkmaULyRk2vYfNOEWOog60biPAP/LRBFwAOcXQReWHXdEgorBgEEAZdVAQUBAQdABu3I1stkhQFPCp5bZbm1Vuu6xYsn6dNSa0Xul6stth0DAQgHAAD/X9y9I/JFBeArkgR3U363cWXXxMCWftS+BDwM9zE4PrgQb4h4BBgWCAAgFiEELm+iyyO1MtcoY0tYZLCPYantlEMFAl5Ydd0CGwwACgkQZLCPYantlEMujwEA5sTwaewZXArM2oK8d5aAmyqGNLcLqC9KVXe0Sb1eYXoBANe5wjJV+gHCjIyHaHpxKf8BOflAlvfmmCj6K+neOwwC

View File

@@ -44,9 +44,9 @@ fn stress_functions(context: &Context) {
// assert!(dc_is_configured(context) != 0, "Missing configured context");
// let setupcode = dc_create_setup_code(context);
// let setupcode_c = CString::yolo(setupcode.clone());
// let setupcode_c = CString::new(setupcode.clone()).unwrap();
// let setupfile = dc_render_setup_file(context, &setupcode).unwrap();
// let setupfile_c = CString::yolo(setupfile);
// 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());