Compare commits

..

399 Commits

Author SHA1 Message Date
bjoern
d0bfb555dd prepare 1.59 (#2614)
* update changelog for 1.59.0

* bump version to 1.59.0
2021-08-20 18:20:06 +02:00
bjoern
6ffaa38b37 add 'device chat about' to now existing status (#2613)
the 'device chat about' was shown as a single message
as at that time, there was just not 'status'.

meanwhile, we have the status option,
and it feels much more natural to get the information there,
esp. as the subtitle on all UIs already read
'Messages in this chat are generated locally' -
and a tap on that will show the hint, without scrolling or so.

this also teaches the user where to find such information -
and the "welcome" chat is less spammy and really starts with the text
"Welcome to Delta Chat!"
2021-08-20 12:30:55 +02:00
bjoern
339d46ecf0 update provider database, add yggmail and mail2tor (#2608)
* allow dotless domains and hostnames

* update provider database

ran ./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
to pull in recent changes from https://github.com/deltachat/provider-db
2021-08-20 10:48:27 +02:00
bjoern
5399c9151d Add Quota to Connectivity View (#2612)
* add imap::get_quota_roots()

* schedule quote-checking job on getting connectivity-html

* get quota and debug print it

* basic quota output

* update quota at most once per minute, emit event on changes

* use more meaningful names

* add some comments, move update_recent_quota() to quota.rs

* show root name only if there are several roots

* make clippy happy, some refactorings

* allow only one update-quota job per time

* add now supported QUOTA to standards.md
2021-08-20 10:40:24 +02:00
bjoern
53cd633e8d add migrated accounts to events emitter (#2607)
successor of #2559
closes #2606
2021-08-16 22:10:34 +02:00
link2xt
ade39fe026 fix: do not set WantsMdn param for outgoing messages
This bug sometimes results in sending read receipts to self in
multi-device setups.

It happens consistently in a setup where the first device is
configured to move messages to DeltaChat folder and the second device
is not. When both devices receive BCC-self message simultaneously, the
first device moves the message to DeltaChat folder, while the second
device tries to mark the message as seen in the Inbox. Regardless of
whether the second device marks the message as seen successfully or
fails because the message is already moved by the first device,
`Job.markseen_msg_on_imap()` sends the read receipt to the From
address.
2021-08-15 20:39:28 +03:00
B. Petersen
b8dad1dbaf add support for socket PLAIN coming from provider-db 2021-08-15 20:28:09 +03:00
dependabot[bot]
72d503fa32 Merge pull request #2602 from deltachat/dependabot/cargo/bitflags-1.3.1 2021-08-14 16:07:47 +00:00
Hocuri
223aeb7b1a Fix: Make emails forwarded by GMX readable (#2600)
Recognizing these emails as forwarded would probably be too complicated and require too much special-casing, but now the user can access the email text via "Show full message".

fix #2599

Co-authored-by: B. Petersen <r10s@b44t.com>
2021-08-14 18:05:17 +02:00
dependabot[bot]
b315c6f6d5 cargo: bump bitflags from 1.2.1 to 1.3.1
Bumps [bitflags](https://github.com/bitflags/bitflags) from 1.2.1 to 1.3.1.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/1.2.1...1.3.1)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-12 21:16:44 +00:00
Hocuri
481276cf46 In dc_maybe_network_lost() directly set the connectivity "Not connected" (#2551)
* In dc_maybe_network_lost() directly set the connectivity "Not connected"

r10s reported that without doing this, the connectivity would stay at
"Connected" for 16 more seconds after network is gone and
dc_maybe_network_lost() was called.

* Set the state for all connections
2021-08-09 17:25:34 +02:00
Hocuri
faab61b0d4 Connectivity view: Only set smtp to "connected" if the last message was actually sent (#2541)
fix #2539

It's always a bit ambiguous which function should do set_err or set last_send_error, I used this rule here:

If some code returns Status::RetryLater, then it sets last_send_error, because Status::RetryLater means that the user won't see the error directly on the message (if we returned Status::Failed(Err(_)), then the message would be shown as failed to the user) Also, smtp_send always sets last_send_error because, well, sending just failed or succeeded.

Also, remove unused field pending_error.
2021-08-09 12:31:33 +02:00
link2xt
20bf41b4e6 Set timestamps for system messages
Previously system messages were always added to the end of the chat,
even if the message triggering them was sent earlier.  This is
especially important for messages about disappearing timer reset
triggered by classic email messages, as they should be placed right
after the message resetting the timer.
2021-08-08 23:03:38 +03:00
link2xt
5a5b80c960 Resultify get_chat_id_by_grpid and create_or_lookup_mailinglist
Use `Option` instead of `Error` to indicate that no chat ID is found.
2021-08-08 16:26:02 +03:00
Simon Laux
ac245a6cb2 accounts: add get_selected_account_id function 2021-08-07 22:39:48 +03:00
dependabot[bot]
126beb62f3 Merge pull request #2529 from deltachat/dependabot/cargo/libc-0.2.98 2021-08-07 18:10:04 +00:00
dependabot[bot]
d79e4a6571 Merge pull request #2527 from deltachat/dependabot/cargo/thiserror-1.0.26 2021-08-07 15:19:23 +00:00
dependabot[bot]
cadc0b2c00 Merge pull request #2585 from deltachat/dependabot/cargo/serde-1.0.127 2021-08-07 15:04:32 +00:00
dependabot[bot]
87071e6d4b cargo: bump thiserror from 1.0.25 to 1.0.26
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.25 to 1.0.26.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.25...1.0.26)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-07 14:49:28 +00:00
dependabot[bot]
057b004553 cargo: bump serde from 1.0.126 to 1.0.127
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.126 to 1.0.127.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.126...v1.0.127)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-07 14:45:51 +00:00
link2xt
4071fe53a0 Fix clippy warnings in deltachat-ffi 2021-08-07 12:03:25 +00:00
link2xt
c3062976c0 Allow clippy::bool_assert_comparison
assert_eq!(var, false) is easier to read than assert!(!var).
2021-08-07 11:48:51 +00:00
link2xt
85efc0ea26 Fix clippy warnings 2021-08-07 11:47:50 +00:00
link2xt
4ef80aaea5 ci: update Rust and Python versions used for testing
- Use latest stable for rustfmt and clippy.
- Update Rust in rust-toolchain and coredeps Docker container to 1.54.0
  and test against it.
- Test against 1.48.0 to ensure libdeltachat can be compiled with Debian
  bullseye "rustc" and "cargo".
2021-08-07 14:22:54 +03:00
link2xt
0f86800f5d ci: build python wheels automatically for py-* tags 2021-08-07 14:22:54 +03:00
link2xt
9c2035538c dc_receive_imf: use None instead of ChatId::new(0) 2021-08-07 00:37:31 +03:00
link2xt
0276938975 ci: update perl
Old miniperl segfaults on the latest manylinux image
2021-08-07 00:00:00 +03:00
Michael Mc Donnell
ee44a162b6 Skip Gmail labels
Gmail labels are not folders and should be skipped. For example, emails
appear in the inbox and under "All Mail" as soon as it is received. The
code used to wrongly conclude that the email had already been moved and
left it in the inbox.
2021-08-04 01:11:35 +03:00
bjoern
c09a83df2b prepare 1.58 (#2584)
* update changelog for 1.58.0

* bump version to 1.58.0
2021-08-02 21:55:32 +02:00
bjoern
8729d2c4aa also move WAL file when moving database (#2583)
the WAL files come from sqlite WAL mode,
they have the same name as the database file with an extra `-wal` suffix.

according to https://sqlite.org/wal.html#the_wal_file ,
the WAL file is part of the persistent state of the database
and should be kept with the database if the database is copied or moved.
2021-08-02 20:43:46 +02:00
gerryfrancis
fdf3397437 Too many words (#2582) 2021-08-02 15:31:41 +02:00
bjoern
3712524765 prepare 1.57 (#2580)
* update changelog for 1.57.0

* bump version to 1.57.0
2021-08-01 21:08:08 +02:00
Jikstra
0b5c4df432 Add -DCMAKE_INSTALL_PREFIX flag example to README 2021-08-01 17:01:07 +03:00
link2xt
0f0072f5a2 dc_receive_imf: don't create chats when MDNs are received 2021-08-01 13:21:16 +03:00
link2xt
09066571be message: don't ignore invalid arguments to handle_mdn 2021-08-01 13:21:02 +03:00
link2xt
8963dab7a4 message: better error handling in handle_mdn 2021-08-01 13:21:02 +03:00
link2xt
265d54e431 message: bubble up SQL errors in handle_mdn()
Previously handle_mdn() returned Ok(None) in response to SQL errors as
if SQL simply returned no rows.
2021-08-01 13:21:02 +03:00
link2xt
ffb17c4e61 Fix nightly clippy errors 2021-08-01 01:46:17 +03:00
link2xt
44bd9f93b4 job: fix delete_msg_on_imap documentation 2021-07-31 22:42:14 +03:00
link2xt
5287a3de40 dc_receive_imf: avoid cloning rfc724_mid String 2021-07-31 22:23:56 +03:00
link2xt
6f644f5c7c dc_receive_imf: extract insert_msg_id from created_db_entries
This makes add_parts() accept one argument less
2021-07-31 22:23:56 +03:00
link2xt
31b930b2fa add_parts: make mime_in_reply_to and mime_references immutable 2021-07-31 22:23:56 +03:00
link2xt
0574aeb768 dc_receive_imf: return ChatId from add_parts() 2021-07-31 22:23:56 +03:00
link2xt
bf68bc14a4 dc_receive_imf: remove dead code
mimeparser ensures that mails have at least one part.

Besides that, add_parts() handles the case of no parts just fine.
2021-07-31 22:23:56 +03:00
TsT
c380647c12 anchor link fix (#2569) 2021-07-29 19:57:06 +02:00
Friedel Ziegelmayer
e22a9999d7 fix(message): make markseen_msgs async compatible
Otherwise this method can not be called from an actually spawned async method, as `PreparedStatement` is `!Send`
2021-07-29 15:47:52 +02:00
bjoern
57870ec54a remove archived count suffix (#2566)
"Archived Chats" also contain old contact requests now
(an maybe new one if the user archives them),
so, there may be easily some thousands chats in "Archived Chats" -
and the count has no real meaning to the user,
esp. as we not even display the number of "Real Chats".

Might be that this "Link" will go away anyway sooner or later,
however, for now, it is just fine to remove the badge counter.
2021-07-28 17:53:12 +02:00
bjoern
a6e1dc4f16 update provider database (#2565)
ran ./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
to pull in recent changes from https://github.com/deltachat/provider-db
2021-07-28 15:22:02 +02:00
holger krekel
fc441d4a44 hide URL for test account creation (#2560)
* hide URL for test account creation

* Update python/README.rst

Co-authored-by: bjoern <r10s@b44t.com>
2021-07-28 15:13:20 +02:00
bjoern
9a77a7b66f fix archiving requests (#2563)
* add a test for archived requests

* fix archived requests

* move requests but the last one to "Archived Chats"

this way, the app looks familiar after the contact request upgrade.
the subselect was copied from the old get_last_deaddrop_fresh_msg()
(which was removed by the contact request upgrade #2514)

* just move all old requests to "Archived Chats"

ux-wise, the advantage of keeping the last one is questionable,
one may think, always the last one is shown in chatlist.

showing _all_ fresh request is not doable
as past cores did not really take care of that
and the db-state is not consistent in that regard.

that would make the already complicated code even more complicated,
so we decided to go the easy way.
2021-07-28 14:56:54 +02:00
link2xt
5856936f49 accounts: update EventEmitter on add_account (#2559)
* accounts: update EventEmitter on add_account

* accounts: do not lock waiting for EventEmitter in dc_accounts_add_account

Otherwise dc_accounts_add_account blocks until an event arrives on
some existing account.
2021-07-27 15:27:08 +02:00
link2xt
532060d8b7 Update async-smtp
New version supports @yggmail addresses.
2021-07-24 03:42:18 +03:00
Robert Schütz
0691aa3d2c add back unwrap_or_else() 2021-07-26 01:57:47 +03:00
Robert Schütz
ef9fbf9eba allow installing lib and include under different prefixes 2021-07-26 01:57:47 +03:00
Jikstra
3647aac4e6 Add information about system wide libdeltachat 2021-07-26 00:04:02 +03:00
link2xt
f88f4155ae Update changelog 2021-07-24 15:58:14 +03:00
link2xt
065b574d93 Remove deaddrop chat
Contact request chats are not merged into a single virtual "deaddrop"
chat anymore. Instead, they are shown in the chatlist the same way as
other chats, but sending of messages to them is not allowed and MDNs
are not sent automatically until the chat is "accepted" by the user.

New API:
- dc_chat_is_contact_request(): returns true if chat is a contact
request.  In this case option to accept and block the chat via
dc_accept_chat() and dc_block_chat() should be shown in the UI.
- dc_accept_chat(): accept contact request and unblock the chat
- dc_block_chat(): decline contact request and block the chat

Removed API:
- dc_create_chat_by_msg_id(): deprecated 2021-02-07 in favor of
  dc_decide_on_contact_request()
- dc_marknoticed_contact(): deprecated 2021-02-07 in favor of
  dc_decide_on_contact_request()
- dc_decide_on_contact_request(): this call requires a message ID from
  deaddrop chat as input. As deaddrop chat is removed, this call can't
  be used anymore.
- dc_msg_get_real_chat_id(): use dc_msg_get_chat_id() instead, the
  only difference between these calls was in handling of deaddrop chat
- removed DC_CHAT_ID_DEADDROP and DC_STR_DEADDROP constants
2021-07-24 15:58:14 +03:00
dependabot[bot]
5c36b6e119 Merge pull request #2523 from deltachat/dependabot/cargo/indexmap-1.7.0 2021-07-23 23:59:43 +00:00
dependabot[bot]
cd0da723ce cargo: bump indexmap from 1.6.2 to 1.7.0
Bumps [indexmap](https://github.com/bluss/indexmap) from 1.6.2 to 1.7.0.
- [Release notes](https://github.com/bluss/indexmap/releases)
- [Commits](https://github.com/bluss/indexmap/compare/1.6.2...1.7.0)

---
updated-dependencies:
- dependency-name: indexmap
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-23 23:16:08 +00:00
dependabot[bot]
49fc72fa42 cargo: bump libc from 0.2.97 to 0.2.98
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.97 to 0.2.98.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.97...0.2.98)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-23 23:15:57 +00:00
dependabot[bot]
a1aaa1e0b4 Merge pull request #2555 from deltachat/dependabot/cargo/futures-0.3.16 2021-07-23 23:12:44 +00:00
dependabot[bot]
1eab99df56 Merge pull request #2531 from deltachat/dependabot/cargo/anyhow-1.0.42 2021-07-23 23:12:10 +00:00
dependabot[bot]
d9caf5853d Merge pull request #2546 from deltachat/dependabot/cargo/sha-1-0.9.7 2021-07-23 23:11:45 +00:00
dependabot[bot]
8869c34539 Merge pull request #2554 from deltachat/dependabot/cargo/syn-1.0.74 2021-07-23 23:11:13 +00:00
dependabot[bot]
05bb25c645 cargo: bump futures from 0.3.15 to 0.3.16
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.15 to 0.3.16.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.15...0.3.16)

---
updated-dependencies:
- dependency-name: futures
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-23 21:11:31 +00:00
dependabot[bot]
b340459752 cargo: bump syn from 1.0.73 to 1.0.74
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.73 to 1.0.74.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.73...1.0.74)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-22 21:10:25 +00:00
bjoern
980d2a9433 add dc_accounts_maybe_network_lost() (#2550) 2021-07-22 11:29:51 +02:00
bjoern
5f365b259b tweak connectivity html (#2549)
* escape strings added to html

* use more common emojis for connectivity report

all emojis are from 2010 and older now.
an alternative would have been to use css,
however, that may have other issues
and as the whole report is subject to change anyway,
i go for the easy solution.

* use 'modern' meta pattern, remove unused div and styles

* use css instead emojis; looks better that way

also, we have the same look on all systems.

* add connectivity command to repl tool
2021-07-21 23:02:09 +02:00
dependabot[bot]
b070198063 cargo: bump sha-1 from 0.9.6 to 0.9.7
Bumps [sha-1](https://github.com/RustCrypto/hashes) from 0.9.6 to 0.9.7.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha-1-v0.9.6...sha-1-v0.9.7)

---
updated-dependencies:
- dependency-name: sha-1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-19 21:13:09 +00:00
Hocuri
6e7f63dba7 Fix which chats messages are assigned to (#2465)
fix https://github.com/deltachat/deltachat-core-rust/issues/2463
fix #2116

The email could be private (i.e. only sent to me) or non-private (i.e. also sent to sb else). Also, it could be a classical email or a chat message. Below, I'm describing for each of the four combinations whether they should be assigned to a chat by create_or_lookup_group() or lookup_chat_by_reply(). Because I needed to use these function names a lot, I shortened them to l:group() and l:by_reply() in this PR description.

(!) means todo, (! -> Done) means I fixed something.

## Private classical email: 
l:group() and l:by_reply() must both take care not to put it into group
l:group() no (! -> Done), l:by_reply no (! -> Done)
except for classical MUA replies to two-member-groups, they should be put into the parent group

### wrt alias-support:

A private classical email is very probably not going to be an answer to email that went to an alias address:

Suppose Alice writes to support@example.com.
support@example.com forwards to bob@example.com and claire@example.com.
When Bob answers, he will _probably_ answer `To: alice@example.com, support@example.com` (=> it's a non-private classical email).

With this PR, if he does only answer `To: alice@example.com`, (=> it's a private classical email), Alice's DC will show the answer in the private chat with Bob. Which actually makes more sense than showing it in the support@example.com chat I think. Also, if it was shown in the support@example.com chat, then Alice would answer in the support@example.com chat, then Claire would get Alice's message but not Bob's and therefore miss some context.
That being said - **I could change this**. Pretty easily actually, I would just have to remove the call to `is_probably_private_reply()` from `lookup_chat_by_reply()`. Didn't think through all edge cases yet, but should work.

## Private chat message:
l:group() has to put private chat messages into the group: It won't mistakenly put a message there (because it can rely on Chat-Group-Id). Currently, `is_probably_private_reply()` returns true for private chat messages, but l:group still has to put them into the group chat. (it's not nice that the function called is_probably_private_reply returns true for all private chat messages, but I didn't find any nicer solution) l:by_reply() must not assign it to sth special
l:group yes, l:by_reply no

By the way, for chat messages, `try_getting_grpid()` doesn't look at InReplyTo or References, in order not to put private chat-message replies into the group chat.

For alias-support, the same goes as for the private classical emails.

## Non-private classical email:
Just put it into any group, and if there is none, create one. 

_Off-topic:
We currently don't look at the recipients lists, which means that a message can easily be assigned to a group although it was not sent to all group members. One day we could somehow compare the recipients list with the members list, but that needs some more discussing (esp. what do we do if they don't match? Create a new group? Show a hint in the UI?) and is nothing for a bug-fixing PR like this one._

l_group yes, l_by_reply yes, even for outgoing messages (! -> Done, this also was the issue reported by @gerryfrancis in the testing group:

Outgoing messages were not put into a chat by In-Reply-To/References, which is correct for chat messages, but for non-private classical outgoing emails, a new ad-hoc group was created everytime.

So, I added this: https://github.com/deltachat/deltachat-core-rust/pull/2465/files#diff-e7606b521f6710ddc6e5236ba5d7eefc917b7ad744b9e71762fd42830c55485bR703-R711)

## Non-private chat message:
Can be put into chat by l:by_reply() because it must be a group message
l:group yes, l:by_reply yes (to make alias support work if the support person uses DC)

Nothing to test or fix here; we have to put chat group messages into the group, which is trivial. And we have to make sure that alias-support works, which already was well tested.
2021-07-19 16:02:11 +02:00
link2xt
eff64ed9b0 Remove strict domain checks for EmailAddress::from_str
They prevent "user@localhost" addresses from being parsed, which are
useful for running online tests against local server.
2021-07-17 22:27:27 +03:00
Simon Laux
49acfd90eb update core changelog 2021-07-17 20:06:11 +03:00
link2xt
aec8332544 mimeparser: use mailparse to parse RFC 2231 filenames
mailparse supports RFC 2231 since version 0.13.5, so there is no need
for our own code to support it.
2021-07-17 14:31:16 +03:00
bjoern
188353d581 add in-doc links for DC_EVENT_CONNECTIVITY_CHANGED, list unused param as for the other events (#2542) 2021-07-17 12:39:05 +02:00
Simon Laux
3bd5b7e604 changelog: update to include previous changes (#2532)
* changelog: update to include prevous changes
and use new format/structure

* changelog: capitalize API

* changelog: add a missing pr refernece

* Apply suggestions from code review

Co-authored-by: Hocuri <hocuri@gmx.de>

Co-authored-by: Hocuri <hocuri@gmx.de>
2021-07-15 14:00:36 +02:00
Hendrik Jansen
61e1e18088 Merge pull request #2479 from deltachat/can_send_group_fix
Fix can_send for users not in a group
2021-07-15 11:50:32 +02:00
hendrik
a5065c21af fixed can_send() for users not in group 2021-07-14 23:10:58 +02:00
dependabot[bot]
cd958c6a33 cargo: bump anyhow from 1.0.41 to 1.0.42
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.41 to 1.0.42.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.41...1.0.42)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-09 21:12:18 +00:00
Hocuri
308403ad99 Connectivity view (instead of spamming the user with error_network when sth fails) (#2319)
See https://support.delta.chat/t/discussion-how-to-show-error-states/1363/10 <!-- comment -->

It turns out that it's pretty easy to distinguish between lots of states (currently Error/NotConnected, Connecting…, Getting new messages… and Connected). What's not that easy is distinguishing between an actual error and no network, because if the server just doesn't respond, it could mean that we don't have network or that we are trying ipv6, but only ipv4 works.

**WRT debouncing:**

Sending of EVENT_CONNECTIVITY_CHANGED is not debounced, but emitted every time one of the 3 threads (Inbox, Mvbox and Sentbox) has a network error, starts fetching data, or is done fetching data.
This means that it is emitted:
- 9 times when dc_maybe_network() is called or we get network connection
- 12 times when we lose network connection

Some measurements: dc_get_connectivity() takes a little more than 1ms (in my measurements back in March), dc_get_connectivity_html() takes 10-20ms. This means that it's no immmediate problem to call them very often, might increase battery drain though. For the UI it may be a lot of work to update the title everytime; at least Android is smart enough to update the title only once.

Possible problems (we don't have to worry about them now I think):
- Due to the scan_folders feature, if the user has lots of folders, the state could be "Connecting..." for quite a long time, generally DC seemed a little unresponsive to me because it took so long for "Connecting..." to go away. Telegram has a state "Updating..." that sometimes comes after "Connecting...".

To be done in other PRs:
- Better handle the case that the password was changed on the server and authenticating fails, see https://github.com/deltachat/deltachat-core-rust/issues/1923 and https://github.com/deltachat/deltachat-core-rust/issues/1768
- maybe event debouncing  (except for "Connected" connectivity events)

fix https://github.com/deltachat/deltachat-android/issues/1760
2021-07-08 22:50:11 +02:00
Sebastian Klähn
599be61566 Merge pull request #2526 from deltachat/fix_2325
Fix #2325 (Stickerforwarding)
2021-07-06 20:50:07 +02:00
Sebastian Klähn
64088f02a2 format 2021-07-06 20:08:40 +02:00
Sebastian Klähn
77aa8b2c3f remove unnecessary function-args 2021-07-06 20:05:18 +02:00
Sebastian Klähn
5bffdc6bbf use ?-operator instead of unwrap() 2021-07-06 20:04:35 +02:00
Sebastian Klähn
350fe06ea9 fix tests 2021-07-06 17:26:16 +02:00
Sebastian Klähn
e100dca348 tests 2021-07-05 22:25:57 +02:00
Sebastian Klähn
f1c4c40aec make fix 2021-07-05 22:25:51 +02:00
link2xt
f96d04e80f ci: trigger doxygen rebuild on every commit 2021-07-03 17:57:43 +03:00
link2xt
c1d3e9358d ci: remove references to CircleCI
It is not used anymore.
2021-07-03 17:57:43 +03:00
B. Petersen
e77651f2f5 clarify docs 2021-06-30 00:02:15 +02:00
B. Petersen
056f3ecf03 remove dc_accounts_import_account() api
in most (all?) UIs, import/export works on an already created account,
so, dc_accounts_import_account() does not really help here -
but adds some noise and confusion
eg. as for the other dc_accounts_t functions,
the corrsponding dc_context_t functions must not be called.

if really a new account is required for import,
it seems to be easier to call add_account() before import.
2021-06-30 00:02:15 +02:00
link2xt
8700cf0aba dc_receive_imf: remove cleanup() closure
Do not send any events in case of `add_parts` error.
2021-06-28 09:16:10 +03:00
link2xt
a6ad457065 dc_receive_imf: fix a typo ("reveive") 2021-06-27 20:52:35 +03:00
link2xt
f113b43046 Use current timestamp instead of 0 for messages without Date:
Otherwise receiving a message without `Date:` in an empty chat pushes
it to the bottom of chatlist.
2021-06-26 23:20:07 +03:00
link2xt
0b3eece26d Use smeared timestamps for chat creation times 2021-06-26 23:20:07 +03:00
link2xt
8ce9a78d6c chatlist: resultify get_msg_id, get_summary and get_summary2
Avoid using MsgId::new(0) in place of `None` in the Rust part.
Zero ID is only used in FFI part now.
2021-06-26 17:04:55 +03:00
link2xt
ad266fe82f dc_receive_imf: exit early if Message-ID is duplicate
Do not process names, avatars, location XMLs, message signature
etc. for duplicate messages.

Previously only `add_parts` was stopped early, but not
`dc_receive_imf`. Also, `dc_receive_imf` processed From: and To:
fields and applied names to contacts even before checking the
Message-ID.

Fake Message-ID generation procedure is changed to operate on raw
header values to avoid interacting with the database.
2021-06-26 15:20:47 +03:00
B. Petersen
15c38ba395 token::save() resultified, doc updated 2021-06-25 23:09:31 +02:00
B. Petersen
70e776e407 refine general dc_check_qr() documentation 2021-06-25 23:09:31 +02:00
B. Petersen
6b5ba35d5b make clippy happy 2021-06-25 23:09:31 +02:00
B. Petersen
7b9e54be56 return unique token for new qr codes
as by reviving qr codes,
there may be more than one token for a chat,
ensure, the most recent token and only one token is returned
by the sql-command for looking up tokens
(used for generating new codes)
2021-06-25 23:09:31 +02:00
B. Petersen
6202f85a6f test withdrawing qr codes 2021-06-25 23:09:31 +02:00
B. Petersen
8ac2bd0298 handle withdraw/revive qr code actions 2021-06-25 23:09:31 +02:00
B. Petersen
3f00a6efbe allow token::save() to handle existing tokens 2021-06-25 23:09:31 +02:00
link2xt
a411fe1e01 Remove InvalidMsgId error type 2021-06-20 17:52:30 +03:00
link2xt
8ea773628d Use anyhow for key.rs error handling 2021-06-19 22:52:32 +03:00
link2xt
a47c0486ae Store mime_headers as BLOBs
Raw MIME messages may contain non-ASCII characters. Attempting to
store them as TEXT by using String::from_utf8_lossy results in
non-ASCII characters being replaced with Unicode U+FFFD "REPLACEMENT
CHARACTER" which is later incorrectly decoded when attempting to parse
`mime_headers` content into HTML.
2021-06-19 17:49:26 +03:00
link2xt
c08df8d3da Do not count info messages for deaddrop chat
Info messages are not displayed in contact requests, so they should
not be counted in get_msg_cnt() and get_fresh_msg_cnt() too.
2021-06-19 17:40:07 +03:00
link2xt
1a830c23b5 Do not hide outgoing messages from contact requests
Normally they should not end up in contact requests, but if they do,
we want to show them. Otherwise it is completely impossible to see
them until the chat is moved out of contact requests.
2021-06-19 17:40:07 +03:00
link2xt
18ace81842 Create chats for outgoing classic mails
Previously chats created by outgoing classic emails went into contact
requests (deaddrop). Outgoing messages are not shown in contact
requests, so created chat was not shown anywhere. With this fix chat
is created both for outgoing classic emails and outgoing chat emails.
2021-06-19 17:40:07 +03:00
link2xt
838957badd Do not hide classic emails from contact requests on setting change
Since classical messages are not deleted when show_emails setting is
set to "0" and remain in the database, they should be shown
somewhere. Otherwise they may reappear later when the setting is
enabled again.
2021-06-19 17:40:07 +03:00
link2xt
f820671d53 Use Auto-Submitted: auto-generated header to identify bots
New `dc_msg_is_bot()` C API and corresponding `Message.is_bot()`
Python API can be used to check if incoming message is sent by a bot,
e.g. to avoid two echo bots replying indefinitely to each other.

"Bot" flag is not set for outgoing messages, but may be set for
BCC-self messages. For now documentation says that `dc_msg_is_bot()`
return value is unspecified for outgoing messages. It can be better
specified later if needed for specific applications, e.g. sharing an
account with a helper bot.
2021-06-19 17:36:20 +03:00
Simon Laux
bf61c16dc1 set_draft message changed event returns now draft's msg id
set_draft message changed event returns now draft msg id
instead of 0
2021-06-19 03:25:18 +03:00
link2xt
96f0e47091 Fix a DC_DESIRED_TEXT_LEN comment typo ("usind") 2021-06-19 03:15:57 +03:00
B. Petersen
514c4bc8a7 test removing the last, seletected account
this just result again in no accounts.
before, get_selected_account() has paniced.
2021-06-19 02:31:57 +03:00
B. Petersen
b53613d1e0 let get_selection_account return an Option<> 2021-06-19 02:31:57 +03:00
B. Petersen
4c4f24fb35 avoid creating a default account, adapt tests
the default account created by dc_accounts_new()
is annoying when doing a migrate/import account;
in this case, you must not forget to remove the default account.
in between you have two account,
and accounts are swiched.

it seems to be better
to just call add_account() on a new account object as needed.
2021-06-19 02:31:57 +03:00
B. Petersen
475fa24876 move links to angle brackets to avoid rustdoc errors 2021-06-19 01:39:04 +03:00
link2xt
cf8736da48 Concourse CI pipeline to replace Circle CI 2021-06-15 00:03:00 +03:00
B. Petersen
a638259c36 add a test to make sure, the selected account is persisted on reopening 2021-06-14 22:07:35 +02:00
link2xt
d821cdf1c8 Improve drafts detection
systemli.org seems not to include Received: header in messages sent to
self, including BCC-self and messages sent to Saved message chat. This
results in all these messages not being displayed on other devices in
multi-device setting. To work around this problem we restrict draft
detection based on Received: header to classical mails.

Delta Chat already implements proper draft detection based on the
\Draft message flag and \Drafts folder flag. However, Thunderbird does
not set \Draft flag when message is stored as "Template". To make
draft detection for Thunderbird more robust, in case Received: header
check is removed altogether later, we check for X-Mozilla-Draft-Info
header which Thunderbird sets both for "Drafts" and "Templates".
2021-06-14 19:34:24 +03:00
dependabot[bot]
62e9fbf68c Merge pull request #2495 from deltachat/dependabot/cargo/once_cell-1.8.0 2021-06-14 14:27:46 +00:00
dependabot[bot]
15664be4f6 cargo: bump once_cell from 1.7.2 to 1.8.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.7.2 to 1.8.0.
- [Release notes](https://github.com/matklad/once_cell/releases)
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.7.2...v1.8.0)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-14 08:07:26 +00:00
link2xt
62388514dd chat: make get_msg_cnt() and get_fresh_msg_cnt() work for deaddrop
Also do not count hidden messages in get_msg_cnt().
2021-06-13 23:05:08 +03:00
link2xt
ad7c7e90b3 Resultify Contact::block 2021-06-13 17:30:42 +03:00
Hocuri
b16785bb62 Tweak test 2021-06-12 16:03:55 +02:00
Hocuri
d12d9d94d6 Improve test_migration_flags, add EvTracker to test_utils 2021-06-12 16:03:55 +02:00
B. Petersen
991d15615e fix minor dbversion inconsistencies
probably they come in by the latest sqlx<->rusqlite moves,
however, as they are followed by subsequent migrations,
that should not have been a big bug in the past
(maybe unless the app was killed at a bad moment)
2021-06-12 16:03:55 +02:00
B. Petersen
5dee1efa59 add missing dbversion update 2021-06-12 16:03:55 +02:00
B. Petersen
1870684c43 add a test that catches some cases where dbversion was forgotten to update 2021-06-12 16:03:55 +02:00
dependabot[bot]
1803db2dfe Merge pull request #2488 from deltachat/dependabot/cargo/libc-0.2.97 2021-06-11 22:42:22 +00:00
dependabot[bot]
7fee3d995c Merge pull request #2478 from deltachat/dependabot/cargo/syn-1.0.73 2021-06-11 22:36:25 +00:00
dependabot[bot]
4b62500989 cargo: bump syn from 1.0.72 to 1.0.73
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.72 to 1.0.73.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.72...1.0.73)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-11 22:19:21 +00:00
dependabot[bot]
8f2cb1e8ab cargo: bump libc from 0.2.95 to 0.2.97
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.95 to 0.2.97.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.95...0.2.97)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-11 22:19:07 +00:00
dependabot[bot]
72ebd83479 Merge pull request #2477 from deltachat/dependabot/cargo/itertools-0.10.1 2021-06-11 22:17:42 +00:00
dependabot[bot]
2842042304 Merge pull request #2484 from deltachat/dependabot/cargo/anyhow-1.0.41 2021-06-11 22:17:04 +00:00
Hocuri
25fed9ab52 comment 2021-06-11 13:09:22 +02:00
Hocuri
751b9add09 remove unneeded files 2021-06-11 13:09:22 +02:00
Hocuri
b727190da5 test low media quality a little more 2021-06-11 13:09:22 +02:00
Hocuri
368fa9fc44 Test image rotation 2021-06-11 13:09:22 +02:00
B. Petersen
c07c5bb358 create own topic for Voice messages, update year 2021-06-11 13:09:04 +02:00
B. Petersen
67f8fb4b66 update spec to Chat-Content: sticker 2021-06-11 13:09:04 +02:00
B. Petersen
056721b916 update spec to new Chat-User-Avatar usage 2021-06-11 13:09:04 +02:00
B. Petersen
4fe3a80f96 allow stickers with gif-images 2021-06-11 13:08:48 +02:00
B. Petersen
6c530b4c77 test sending and receiving stickers 2021-06-11 13:08:48 +02:00
B. Petersen
c616b65ce4 allow sending stickers via repl tool 2021-06-11 13:08:48 +02:00
dependabot[bot]
50f680a00b cargo: bump anyhow from 1.0.40 to 1.0.41
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.40 to 1.0.41.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.40...1.0.41)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-11 06:17:13 +00:00
dependabot[bot]
47e0f224ca cargo: bump itertools from 0.10.0 to 0.10.1
Bumps [itertools](https://github.com/rust-itertools/itertools) from 0.10.0 to 0.10.1.
- [Release notes](https://github.com/rust-itertools/itertools/releases)
- [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-itertools/itertools/compare/v0.10.0...v0.10.1)

---
updated-dependencies:
- dependency-name: itertools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-10 06:26:18 +00:00
B. Petersen
8b872b7e6f bump version to 1.56.0 2021-06-07 21:55:51 +02:00
B. Petersen
29356a6ca8 update changelog for 1.56.0 2021-06-07 21:55:51 +02:00
link2xt
c5539de4da More robust In-Reply-To parsing
Use `parse_message_id` on the header contents before using it.

Database still stores the raw value.
2021-06-07 21:38:00 +02:00
B. Petersen
b017af78ce update provider database
ran `./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs`
to pull in recent changes from https://github.com/deltachat/provider-db
2021-06-07 16:19:05 +02:00
Hocuri
3b897eac53 Fix downscaling images (#2469)
* Add failing test

* typo

* Fix the bug
2021-06-07 10:14:03 +02:00
link2xt
d8a3014896 securejoin: display error reason if there is any 2021-06-06 23:53:43 +03:00
dependabot[bot]
4209960c0f Merge pull request #2460 from deltachat/dependabot/cargo/strum-0.21.0 2021-06-06 11:12:09 +00:00
dependabot[bot]
04c8622e94 cargo: bump strum from 0.20.0 to 0.21.0
Bumps [strum](https://github.com/Peternator7/strum) from 0.20.0 to 0.21.0.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-06 07:05:19 +00:00
dependabot[bot]
002e33d28c Merge pull request #2464 from deltachat/dependabot/cargo/strum_macros-0.21.1 2021-06-06 07:03:32 +00:00
dependabot[bot]
35aeda3849 Merge pull request #2466 from deltachat/dependabot/cargo/futures-lite-1.12.0 2021-06-06 07:02:50 +00:00
link2xt
af287ee9a8 Do not allow to delete contacts with ongoing chats
Even if chat has no messages, contacts should not be deleted.
Otherwise chat will reference invalid contact ID which can't be loaded
from the database, resulting in broken contact list in group chats.
2021-06-06 09:50:12 +03:00
link2xt
cc3e8c5117 imap: refactor to always create Imap configured
`Imap` structure is always created in a configured state now. There is
no default value for `ImapConfig` anymore.

Also resultify Scheduler::start() to fail on database errors, for
example if IMAP configuration cannot be read from the database during
`start_io()`. Previosuly errors during reading keys such as
`mvbox_watch` were simply ignored and folders were not watched until
the application is completely restarted, now start_io() will fail and
scheduler will only be started at the next start_io() call which
usually happens when app is brought to the foreground.
2021-06-06 09:49:23 +03:00
dependabot[bot]
1127521923 cargo: bump futures-lite from 1.11.3 to 1.12.0
Bumps [futures-lite](https://github.com/smol-rs/futures-lite) from 1.11.3 to 1.12.0.
- [Release notes](https://github.com/smol-rs/futures-lite/releases)
- [Changelog](https://github.com/smol-rs/futures-lite/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/futures-lite/compare/v1.11.3...v1.12.0)

---
updated-dependencies:
- dependency-name: futures-lite
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-04 07:32:19 +00:00
Hocuri
bf7f64d50b Ignore Drafts folder when scanning (#2454)
* Add failing test for #2369

* Completely ignore Drafts folder

fix #2369

* Also ignore messages that have the Draft flag set but are not in the Drafts folder
2021-06-03 21:14:39 +02:00
dependabot[bot]
8380ac28c1 cargo: bump strum_macros from 0.20.1 to 0.21.1
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.20.1 to 0.21.1.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/commits)

---
updated-dependencies:
- dependency-name: strum_macros
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-03 07:23:26 +00:00
dependabot[bot]
d8ba466c6a Merge pull request #2452 from deltachat/dependabot/cargo/thiserror-1.0.25 2021-05-28 23:06:58 +00:00
dependabot[bot]
0c2a3c8347 Merge pull request #2453 from deltachat/dependabot/cargo/libc-0.2.95 2021-05-28 22:43:59 +00:00
Hocuri
e3e2adeea5 Fix all outgoing messages popping up in selfchat (#2456)
Fix https://github.com/deltachat/deltachat-android/issues/1940, fix https://github.com/deltachat/deltachat-core-rust/issues/2220 (I assume these are the same bug)

The problem was:
- Gmail adds a header `Bcc: <self-address>` to our bcc-self message
- `to_id` was just set to the first recipient, in this case _self_
- it was seen that `to_id` is _self_, so it's a self-sent message
2021-05-28 20:02:46 +02:00
Hocuri
46e901be78 Fix benchmark compile error (#2457) 2021-05-28 18:48:13 +02:00
dependabot[bot]
d899a38d17 cargo: bump libc from 0.2.94 to 0.2.95
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.94 to 0.2.95.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.94...0.2.95)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-26 06:30:24 +00:00
dependabot[bot]
b60994b313 cargo: bump thiserror from 1.0.24 to 1.0.25
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.24 to 1.0.25.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.24...1.0.25)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-24 07:55:01 +00:00
link2xt
8b19b6f9fe ci: don't build circleci on branches 2021-05-23 19:34:59 +03:00
link2xt
ea88a567f9 ci: require that tag contains at least 1 character 2021-05-23 19:32:31 +03:00
link2xt
38c23104da ci: build wheels only for tags 2021-05-23 19:30:10 +03:00
link2xt
2a83147d90 tox: fix "test command found but not installed in testenv" warning
Determine architecture from python instead of using sh and uname
2021-05-23 18:40:40 +03:00
link2xt
7e3bfd38f0 scripts: use rust 1.52.1 for docker-coredeps-arm64
Building aarch64 wheels under qemu takes a long time.
rust 1.52.1 includes fix for cargo that may result in
compilation being stuck: https://github.com/rust-lang/cargo/pull/9201
2021-05-23 18:20:54 +03:00
link2xt
8b78e12b36 Set ServerAliveInterval to ping SSH server every 30 seconds
Otherwise connection may be dropped if there is no output for some time.
2021-05-23 13:16:16 +03:00
link2xt
15660f2741 Set ServerAliveInterval to ping SSH server every 30 seconds
Otherwise connection may be dropped if there is no output for some time.
2021-05-23 13:10:24 +03:00
link2xt
03520fbd4b Increase wheel building timeout again 2021-05-23 13:01:21 +03:00
link2xt
b1228cbbe5 ci: increase remote python packaging timeout to 30m 2021-05-23 11:43:30 +03:00
link2xt
09f5b015bb python: support running auditwheels for aarch64 2021-05-23 11:43:30 +03:00
link2xt
67ca93b093 ci: build aarch64 wheels
Also drop python 3.6, aarch64 wheels fail to build there
2021-05-23 10:55:01 +03:00
link2xt
dabf31204c scripts: do not build py310 wheels
There is no py310 cffi wheel yet.
2021-05-23 09:50:24 +03:00
link2xt
c22580e07f scripts: build wheels for python 3.6-3.10
These are all versions supported by manylinux2014 now
2021-05-23 09:26:16 +03:00
link2xt
0028f579b6 Update docker-coredeps to use python3.6
Python 3.5 is removed from the latest manylinux2014 images
2021-05-23 09:24:58 +03:00
link2xt
9522240992 scripts: add docker-coredeps-arm64 2021-05-23 09:05:07 +03:00
link2xt
8ab3415c58 CMakeLists.txt: specify only C language
By default C and CXX are enabled.
This resulted in unnecessary C++ compiler check.
2021-05-22 20:29:02 +03:00
link2xt
e858fca356 dc_receive_imf: add python test for server-side message moving
There was a bugreport for previous versions of DC that BCC-self
messages are not marked as seen if server-side Sieve rule moves them.

This test is an unsuccessful attempt to reproduce the bug,
which was confirmed to be fixed.
2021-05-22 18:53:16 +03:00
link2xt
7d4affcc8d python: disable BCC-self in test_markseen_message_and_mdn
This test checks that MDN is marked as seen on ac1.  Because ac1 also
receives BCC-self, it is possible that event of marking this message
as seen is confused with marking MDN as seen, and the test passes even
if MDN is not marked as seen.  Explicitly disabling BCC-self for ac1
ensures it receives only one message (MDN).

Also start direct_imap as early as possible, before sending the
message. It is possible now, because BCC-self is not sent.
2021-05-22 18:52:55 +03:00
link2xt
60d596bb0e README: remove "remote tests" badge
This CI workflow does not exist anymore.
2021-05-22 16:17:25 +03:00
dependabot[bot]
c57bfde010 Merge pull request #2449 from deltachat/dependabot/cargo/rustyline-8.2.0 2021-05-22 12:37:04 +00:00
dependabot[bot]
afd7e7eaac cargo: bump rustyline from 8.1.0 to 8.2.0
Bumps [rustyline](https://github.com/kkawakam/rustyline) from 8.1.0 to 8.2.0.
- [Release notes](https://github.com/kkawakam/rustyline/releases)
- [Changelog](https://github.com/kkawakam/rustyline/blob/master/History.md)
- [Commits](https://github.com/kkawakam/rustyline/compare/v8.1.0...v8.2.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-21 06:51:43 +00:00
dependabot[bot]
cfc324c95b Merge pull request #2448 from deltachat/dependabot/cargo/rustyline-8.1.0 2021-05-19 23:28:31 +00:00
dependabot[bot]
aa5d6077a8 cargo: bump rustyline from 8.0.0 to 8.1.0
Bumps [rustyline](https://github.com/kkawakam/rustyline) from 8.0.0 to 8.1.0.
- [Release notes](https://github.com/kkawakam/rustyline/releases)
- [Changelog](https://github.com/kkawakam/rustyline/blob/master/History.md)
- [Commits](https://github.com/kkawakam/rustyline/compare/v8.0.0...v8.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-19 23:02:42 +00:00
dependabot[bot]
4a2beac6fc Merge pull request #2447 from deltachat/dependabot/cargo/async-std-resolver-0.20.3 2021-05-19 21:37:01 +00:00
dependabot[bot]
b5dc954408 cargo: bump async-std-resolver from 0.20.2 to 0.20.3
Bumps [async-std-resolver](https://github.com/bluejekyll/trust-dns) from 0.20.2 to 0.20.3.
- [Release notes](https://github.com/bluejekyll/trust-dns/releases)
- [Changelog](https://github.com/bluejekyll/trust-dns/blob/v0.20.3/CHANGELOG.md)
- [Commits](https://github.com/bluejekyll/trust-dns/compare/v0.20.2...v0.20.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-17 07:48:56 +00:00
Hocuri
a9f5077cf9 Make scan_folders work when inbox is not watched (#2446)
When watch_inbox was off, scan_folders failed and a toast "IMAP operation attempted while it is torn down" was shown.

--

The problem was:

When inbox_watch is off, scan_folders() is called at 244260a978/src/scheduler.rs (L107) but connect_configured() is not called before.



* Add test

* Don't only setup handle, but connect_configured() in scan_folders()
2021-05-16 22:18:38 +02:00
link2xt
09280508bc ci: run python tests on github workers 2021-05-16 16:30:40 +03:00
link2xt
4391835a8d qr: remove outdated comment
The comment referred to using Param::SetLongitude instead of "n".
It was fixed in 6bb0c164f9
2021-05-15 17:15:13 +03:00
dependabot[bot]
05f9ac0583 Merge pull request #2440 from deltachat/dependabot/cargo/sha-1-0.9.6 2021-05-15 01:10:27 +00:00
dependabot[bot]
fd9d632cd6 cargo: bump sha-1 from 0.9.5 to 0.9.6
Bumps [sha-1](https://github.com/RustCrypto/hashes) from 0.9.5 to 0.9.6.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha-1-v0.9.5...sha-1-v0.9.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-15 00:56:03 +00:00
dependabot[bot]
c9e626322b Merge pull request #2439 from deltachat/dependabot/cargo/sha2-0.9.5 2021-05-15 00:54:40 +00:00
dependabot[bot]
f343ec47b4 Merge pull request #2438 from deltachat/dependabot/cargo/futures-0.3.15 2021-05-15 00:49:12 +00:00
dependabot[bot]
efb1534d5c Merge pull request #2441 from deltachat/dependabot/cargo/serde-1.0.126 2021-05-15 00:48:13 +00:00
dependabot[bot]
6ec765cad6 cargo: bump serde from 1.0.125 to 1.0.126
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.125 to 1.0.126.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.125...v1.0.126)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-13 07:02:22 +00:00
dependabot[bot]
372a4ee539 cargo: bump sha2 from 0.9.4 to 0.9.5
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.9.4 to 0.9.5.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.9.4...sha2-v0.9.5)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-12 06:25:22 +00:00
dependabot[bot]
418b591602 cargo: bump futures from 0.3.14 to 0.3.15
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.14 to 0.3.15.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.14...0.3.15)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-12 06:25:14 +00:00
B. Petersen
e387b4f4dd bumb version to 1.55.0 2021-05-11 13:58:46 +02:00
B. Petersen
88a10eaf2c update changelog for 1.55.0 2021-05-11 13:58:46 +02:00
dependabot[bot]
4aae12ead7 Merge pull request #2435 from deltachat/dependabot/cargo/backtrace-0.3.59 2021-05-10 16:21:34 +00:00
dependabot[bot]
89c985120b Merge pull request #2433 from deltachat/dependabot/cargo/url-2.2.2 2021-05-10 16:14:23 +00:00
dependabot[bot]
40bb0616da Merge pull request #2432 from deltachat/dependabot/cargo/rusqlite-0.25.3 2021-05-10 16:10:48 +00:00
dependabot[bot]
2b81c274f1 Merge pull request #2434 from deltachat/dependabot/cargo/escaper-0.1.1 2021-05-10 16:07:42 +00:00
dependabot[bot]
0266b70b23 cargo: bump backtrace from 0.3.58 to 0.3.59
Bumps [backtrace](https://github.com/rust-lang/backtrace-rs) from 0.3.58 to 0.3.59.
- [Release notes](https://github.com/rust-lang/backtrace-rs/releases)
- [Commits](https://github.com/rust-lang/backtrace-rs/compare/0.3.58...0.3.59)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 10:30:29 +00:00
dependabot[bot]
9c0d84090e cargo: bump escaper from 0.1.0 to 0.1.1
Bumps [escaper](https://github.com/dignifiedquire/rust-escaper) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/dignifiedquire/rust-escaper/releases)
- [Commits](https://github.com/dignifiedquire/rust-escaper/compare/0.1.0...0.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 10:30:22 +00:00
dependabot[bot]
baf7e98c1e cargo: bump url from 2.2.1 to 2.2.2
Bumps [url](https://github.com/servo/rust-url) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.2.1...v2.2.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 10:30:10 +00:00
dependabot[bot]
2d20a81f22 cargo: bump rusqlite from 0.25.1 to 0.25.3
Bumps [rusqlite](https://github.com/rusqlite/rusqlite) from 0.25.1 to 0.25.3.
- [Release notes](https://github.com/rusqlite/rusqlite/releases)
- [Changelog](https://github.com/rusqlite/rusqlite/blob/master/Changelog.md)
- [Commits](https://github.com/rusqlite/rusqlite/compare/v0.25.1...v0.25.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 10:29:58 +00:00
link2xt
4be4472dfb mimefactory: use format=flowed for read receipt text
This avoids too long lines which are rejected by some spam filters.
2021-05-10 12:07:14 +03:00
link2xt
98c1158cde tox.ini: use py3 env instead of py37
This allows to run `tox `on systems with only a single Python 3
version installed, such as Python 3.9.

CI script scripts/run-python-test.sh specifies `-e py37` explicitly
anyway.
2021-05-10 02:51:06 +03:00
link2xt
3769ad32bd tox.ini: remove outdated comment 2021-05-10 02:49:13 +03:00
Hocuri
1c436777e0 Fix #2429 (message was downloaded multiple times a second), add test (#2430)
The problem was:

If a message has the \Deleted flag set, we ignore it. But we forgot to
update last_uid, so that uid_next was not updated at 47e639b777/src/imap.rs (L730) and the same message was
fetched over and over again.

Fix #2429
2021-05-09 18:43:55 +02:00
link2xt
adac903818 Debloat the binary by using less AsRef arguments
Using `impl AsRef<str>` as the argument instead of `&str` makes it
possible to call the function with `&str`, `String` and other types
that implement `AsRef` trait.

The cost of it is that compiled binary contains mulitple versions of
the same function, one for each variant of types. If function contains
multiple generic `impl AsRef` arguments, the number of versions possibly
compiled into binary grows exponentially with the number of arguments.

Simple way to avoid it is to call `.as_ref()` on the caller side to
convert the argument to `&str`. In most cases even adding a `&` and
relying on `Deref` coercion is sufficient.

This patch changes many functions that accepted `impl AsRef<str>` and
`impl AsRef<Path>` to accept `&str` and `&Path` instead.

In some places `.clone()` calls are removed. Calling `.clone()` on
`String` and passing `String` to a function accepting `impl
AsRef<str>` is completely unnecessary as `&str` reference could be
passed instead. There is no clippy warning against it yet, but
changing argument type to `&str` allowed to find these cases.

The result of debloating is not impressive, several hundred kilobytes
are saved, which is about 3% of the `.so` binary, but the code is
cleaner too.
2021-05-09 16:25:11 +03:00
Hendrik Jansen
03f0659454 Merge pull request #2428 from deltachat/new-branch-test
Correct typo
2021-05-09 11:05:30 +02:00
Hocuri
296c230bc9 Correct typo 2021-05-09 10:27:11 +02:00
link2xt
ffd00978e9 github actions: build windows repl exe 2021-05-08 18:52:01 +03:00
link2xt
a8f58ec2cf deltachat.h: fix a typo 2021-05-07 22:34:17 +03:00
dependabot[bot]
d8a2c05c71 Merge pull request #2421 from deltachat/dependabot/cargo/sha-1-0.9.5 2021-05-07 15:05:33 +00:00
dependabot[bot]
7e4386c197 cargo: bump sha-1 from 0.9.4 to 0.9.5
Bumps [sha-1](https://github.com/RustCrypto/hashes) from 0.9.4 to 0.9.5.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha-1-v0.9.4...sha-1-v0.9.5)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-07 14:50:54 +00:00
dependabot[bot]
f595264418 Merge pull request #2420 from deltachat/dependabot/cargo/sha2-0.9.4 2021-05-07 14:46:15 +00:00
dependabot[bot]
f3e8f5babc cargo: bump sha2 from 0.9.3 to 0.9.4
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.9.3 to 0.9.4.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.9.3...sha2-v0.9.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-06 08:24:39 +00:00
Floris Bruynooghe
d7b4a5fc9e Move module functions to type methods
This moves the module-level lookup and creation functions to the
types, which make the naming more consistent.  Now the lookup_* get_*
and create_* functions all behave similarly.

Peraps even more important the API of the lookup now allows
distinguishing failure from not found.  This in turn is important to
be able to remove reliance on a ChatId with a 0 or "unset" value.  The
locations where this ChatId(0) is still used is in database queries
which should be solved in an independed commit.
2021-05-04 22:32:05 +02:00
Floris Bruynooghe
be413b20f1 Explicit API for creating chats with blocked status
This introduces the explicit ChatIdBlocked struct to more explicitly
create a chat with a blocked status.  It also adds a common shortcut
to ChatId itself which is more natural to use in many cases.
2021-05-04 22:32:05 +02:00
dependabot[bot]
99d9773b75 Merge pull request #2418 from deltachat/dependabot/cargo/syn-1.0.72 2021-05-04 12:26:12 +00:00
dependabot[bot]
633929b84c cargo: bump syn from 1.0.71 to 1.0.72
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.71 to 1.0.72.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.71...1.0.72)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-04 08:26:01 +00:00
link2xt
f42da17a78 Simplify SQL error handling (#2415)
* Remove sql::error submodule

Use anyhow errors instead.

* Remove explicit checks for open SQL connection

An error will be thrown anyway during attempt to execute query.

* Don't use `with_conn()` and remove it

* Remove unused `with_conn_async`

* Resultify markseen_msgs
2021-05-03 23:01:06 +03:00
link2xt
d421670477 scripts/set_core_version.py: suggest using annotated tags
According to `git help tag`, annotated tags are meant for releases.

If tags are not annotated, `git describe` ignores them.
2021-05-03 21:47:10 +03:00
B. Petersen
c4f36836d4 clarify tagging hint 2021-05-03 17:19:15 +02:00
bjoern
553f4c4b88 prepare 1.54 (#2412) 2021-05-03 02:50:20 +03:00
link2xt
30c463e0ba Add extension to avatars base64-encoded in headers 2021-05-03 01:32:37 +02:00
link2xt
d5c1e26354 python: list requests as a requirement
It is used in `testplugin.py`.
2021-05-03 00:16:52 +03:00
Hocuri
b7864f232b Compress avatar to below 20k (#2384)
- Currently, group images are compressed as well because it was easier to implement that way.
- Currently, in the unlikely case that the avatar is compressed down to 20x20 pixels but still bigger than 20KB, the user doesn't get any indication of this, the avatar simply isn't changed (at least on Android).

  If we want to change this, the easiest way is probably to let `dc_set_config()` in the ffi call `error!()` if `Selfavatar` can't be set. The same might make sense for some or all other configs. BUUUUUT: At least Android doesn't show error!() toasts anymore, probably because they were used too often and too spammy.
- The factor by which we scale down if the file is too big is 1.5.
2021-05-02 19:54:13 +02:00
link2xt
8e9d8ae1ec Fix disabling of vendoring in CMakeLists.txt 2021-05-02 16:50:41 +03:00
link2xt
f52c23d1c7 ci: remove CIRCLE_* environment variables from scripts
Instead, allow specifying free-form BUILD_ID from the command line.

scripts/remote_python_packaging.sh still uses CIRCLE_ variables to avoid
changing working CircleCI config.
2021-05-02 15:02:30 +03:00
link2xt
957f942872 Cargo.toml: move "rusqlite/bundled" to "vendored" feature
It is enabled from deltachat-ffi/Cargo.toml by default.

Fix for abac35c872
2021-05-02 13:58:39 +02:00
dependabot[bot]
6971bfc3d4 cargo: bump rustyline from 4.1.0 to 8.0.0 (#2402) 2021-05-01 20:39:12 +00:00
link2xt
16dcd712f0 dependabot: allow 10 pull requests
Default of 5 is too small.
2021-05-01 23:31:41 +03:00
dependabot[bot]
9f337e8be5 Merge pull request #2407 from deltachat/dependabot/cargo/mailparse-0.13.4 2021-05-01 20:12:20 +00:00
dependabot[bot]
c4217ea929 Merge pull request #2408 from deltachat/dependabot/cargo/syn-1.0.71 2021-05-01 20:10:00 +00:00
dependabot[bot]
3a742f1d09 cargo: bump syn from 1.0.67 to 1.0.71
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.67 to 1.0.71.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.67...1.0.71)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 19:54:37 +00:00
dependabot[bot]
ae0dbf024d cargo: bump mailparse from 0.13.2 to 0.13.4
Bumps [mailparse](https://github.com/staktrace/mailparse) from 0.13.2 to 0.13.4.
- [Release notes](https://github.com/staktrace/mailparse/releases)
- [Commits](https://github.com/staktrace/mailparse/commits/v0.13.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 19:54:30 +00:00
link2xt
01d3611f3b check_verified_properties: handle NULL in verified_key_fingerprint
A regression due to switching from/to rusqlite
2021-05-01 22:46:08 +03:00
dependabot[bot]
f1608b503f Merge pull request #2403 from deltachat/dependabot/cargo/dirs-3.0.2 2021-05-01 19:42:06 +00:00
dependabot[bot]
98beb7f40c Merge pull request #2405 from deltachat/dependabot/cargo/syn-1.0.67 2021-05-01 19:40:28 +00:00
dependabot[bot]
574bb8fd7f Merge pull request #2404 from deltachat/dependabot/cargo/regex-1.4.6 2021-05-01 19:39:52 +00:00
dependabot[bot]
f5de2e7684 cargo: bump syn from 1.0.64 to 1.0.67
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.64 to 1.0.67.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.64...1.0.67)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 19:23:55 +00:00
dependabot[bot]
42086ceec5 cargo: bump regex from 1.4.5 to 1.4.6
Bumps [regex](https://github.com/rust-lang/regex) from 1.4.5 to 1.4.6.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.4.5...1.4.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 19:23:50 +00:00
dependabot[bot]
cfb22c23df cargo: bump dirs from 3.0.1 to 3.0.2
Bumps [dirs](https://github.com/soc/dirs-rs) from 3.0.1 to 3.0.2.
- [Release notes](https://github.com/soc/dirs-rs/releases)
- [Commits](https://github.com/soc/dirs-rs/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 19:23:44 +00:00
dependabot[bot]
d49de4b3e4 Merge pull request #2399 from deltachat/dependabot/cargo/futures-0.3.14 2021-05-01 19:19:17 +00:00
dependabot[bot]
540ad71473 cargo: bump futures from 0.3.13 to 0.3.14
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.13 to 0.3.14.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.13...0.3.14)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 19:02:40 +00:00
dependabot[bot]
b27ad955f8 Merge pull request #2400 from deltachat/dependabot/cargo/async-std-1.9.0 2021-05-01 19:00:44 +00:00
link2xt
5546ed772e cargo: update stop-token and async-imap 2021-05-01 21:41:02 +03:00
dependabot[bot]
23e891f051 cargo: bump async-std from 1.8.0 to 1.9.0
Bumps [async-std](https://github.com/async-rs/async-std) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/async-rs/async-std/releases)
- [Changelog](https://github.com/async-rs/async-std/blob/master/CHANGELOG.md)
- [Commits](https://github.com/async-rs/async-std/compare/v1.8.0...v1.9.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 18:39:33 +00:00
dependabot[bot]
7dd5b05a00 cargo: bump backtrace from 0.3.56 to 0.3.58
Bumps [backtrace](https://github.com/rust-lang/backtrace-rs) from 0.3.56 to 0.3.58.
- [Release notes](https://github.com/rust-lang/backtrace-rs/releases)
- [Commits](https://github.com/rust-lang/backtrace-rs/compare/0.3.56...0.3.58)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 21:38:04 +03:00
dependabot[bot]
b7d274e0f9 cargo: bump async-std-resolver from 0.19.7 to 0.20.2
Bumps [async-std-resolver](https://github.com/bluejekyll/trust-dns) from 0.19.7 to 0.20.2.
- [Release notes](https://github.com/bluejekyll/trust-dns/releases)
- [Changelog](https://github.com/bluejekyll/trust-dns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluejekyll/trust-dns/compare/v0.19.7...v0.20.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 21:37:43 +03:00
dependabot[bot]
437b7ef1f1 cargo: bump anyhow from 1.0.39 to 1.0.40
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.39 to 1.0.40.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.39...1.0.40)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 21:25:03 +03:00
dependabot[bot]
6934947d0d cargo: bump pretty_assertions from 0.6.1 to 0.7.2
Bumps [pretty_assertions](https://github.com/colin-kiegel/rust-pretty-assertions) from 0.6.1 to 0.7.2.
- [Release notes](https://github.com/colin-kiegel/rust-pretty-assertions/releases)
- [Changelog](https://github.com/colin-kiegel/rust-pretty-assertions/blob/main/CHANGELOG.md)
- [Commits](https://github.com/colin-kiegel/rust-pretty-assertions/compare/v0.6.1...v0.7.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 19:53:26 +03:00
dependabot[bot]
d920ec96fa cargo: bump proptest from 0.10.1 to 1.0.0
Bumps [proptest](https://github.com/altsysrq/proptest) from 0.10.1 to 1.0.0.
- [Release notes](https://github.com/altsysrq/proptest/releases)
- [Changelog](https://github.com/AltSysrq/proptest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/altsysrq/proptest/compare/v0.10.1...1.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 19:53:11 +03:00
dependabot[bot]
ebfeec8907 cargo: bump async-trait from 0.1.48 to 0.1.50
Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.48 to 0.1.50.
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.48...0.1.50)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 19:49:45 +03:00
dependabot[bot]
6d064dca84 cargo: bump quick-xml from 0.18.1 to 0.22.0
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.18.1 to 0.22.0.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.18.1...v0.22.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 19:46:50 +03:00
dependabot[bot]
2c2fad6f28 Merge pull request #2391 from deltachat/dependabot/cargo/kamadak-exif-0.5.4 2021-05-01 16:37:21 +00:00
link2xt
60b4f3f21a chat: use anyhow::Result to avoid repeating , Error> 2021-05-01 19:29:17 +03:00
dependabot[bot]
c128e54896 cargo: bump kamadak-exif from 0.5.3 to 0.5.4
Bumps [kamadak-exif](https://github.com/kamadak/exif-rs) from 0.5.3 to 0.5.4.
- [Release notes](https://github.com/kamadak/exif-rs/releases)
- [Commits](https://github.com/kamadak/exif-rs/compare/0.5.3...0.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-01 16:12:17 +00:00
link2xt
0ea6f72624 github: add Dependabot configuration
Configuration file is documented at https://docs.github.com/en/code-security/supply-chain-security/enabling-and-disabling-version-updates
2021-05-01 19:10:51 +03:00
link2xt
855b6b18fd contact: synchronize status between devices
This feature is similar to existing avatar synchronization.

Whenever encrypted BCC-to-self copy of chat message is received, status
setting is updated with the signature of the message.
2021-05-01 19:08:06 +03:00
link2xt
abac35c872 Make it possible to disable bundled SQLite
Also disable it in CMakeLists.txt which is used to install libdeltachat
system-wide.
2021-05-01 19:07:25 +03:00
link2xt
17ad4e99ee Update rusqlite from 0.24 to 0.25 2021-05-01 19:07:25 +03:00
link2xt
c5aef03008 Update changelog 2021-05-01 17:05:18 +03:00
link2xt
c7f2a43654 Update changelog 2021-05-01 15:27:08 +03:00
Simon Laux
19176d9d47 add "Forwarded:" to summary2,
this affects notifications and the chatlist
2021-05-01 15:08:27 +03:00
link2xt
db1a7023eb scripts: remove old/run-python.sh
It is replaced by scripts/run_all.sh
2021-05-01 15:04:07 +03:00
link2xt
ae31b5895b Remove old/gh-actions-rust.yml 2021-05-01 14:54:59 +03:00
link2xt
35b6dd797d tox.ini: pin breathe version
Python 3.5 compatibility is broken in the latest version.
2021-05-01 14:45:19 +03:00
link2xt
1d708de82f docker-coredeps: update to manylinux2014
Rust does not work on manylinux2010 due to old GNU C Library.

We have been using manylinux2014 on CI machine already, but this change
was never commited.
2021-05-01 14:20:37 +03:00
B. Petersen
f7139331e7 test that the correct headers are moved 2021-05-01 07:40:56 +03:00
link2xt
131651cc02 base64-encode avatar into the hidden header 2021-05-01 07:40:56 +03:00
link2xt
bba437523a Process Chat-User-Avatar headers with embedded base64 images 2021-05-01 07:40:56 +03:00
link2xt
f76bc44cdc mimeparser: parse hidden headers 2021-05-01 07:40:56 +03:00
link2xt
f6eb169c60 mimefactory: implement hidden headers 2021-05-01 07:40:56 +03:00
link2xt
e15ec2eb7a mimefactory: create MessageHeaders structure 2021-05-01 07:40:56 +03:00
link2xt
b3b46688fc Better comments for protected_headers and unprotected_headers
Make it clear that protected_headers are protected only
opportunistically and will go into IMF header section if the message
is not encrypted.
2021-05-01 07:40:56 +03:00
link2xt
9faf4a5fa7 python: remove unused imports 2021-04-30 10:56:27 +03:00
Robert Schütz
628c30f130 python: fix build against system library 2021-04-30 10:35:42 +03:00
B. Petersen
f40b557454 add tests for marknoticed_chat(), markseen_msgs() and get_state() 2021-04-26 23:15:26 +02:00
B. Petersen
e1b9e8f2c9 stop more times in repl tool 2021-04-26 23:15:26 +02:00
B. Petersen
65c17cfea2 dc_markseen_msgs() should not handle deaddrop
messages are marked as 'noticed' already when the chat is opened
as all UIs call dc_marknoticed_chat();

also marking messages as 'noticed' when dc_markseen_msgs() is called
is not needed therefore - but stands in the way of further improvements
for the deaddrop, eg. UI may let the user decide when the deaddrop
can be removed from chatlist ('All done' button or so)
2021-04-26 23:15:26 +02:00
B. Petersen
39d3a594af let dc_marknoticed_chat() also handle the virtual deaddrop chat 2021-04-26 23:15:26 +02:00
link2xt
949e671d9c ci: fail on cargo check warnings 2021-04-26 01:11:32 +03:00
link2xt
eef51f064a sql: set PRAGMA synchronous=normal
It was set in `sqlx` code, but not in `rusqlite` mode.

synchronous=FULL makes benchmark that write to the database a lot slower.
2021-04-25 23:44:16 +03:00
B. Petersen
143c5e6249 re-add test_db_reopen() for rusqlite
so that we do not forget about that test
and the issues we had with that :)
2021-04-25 22:33:14 +03:00
link2xt
8610b0c945 sql: switch from sqlx to rusqlite 2021-04-25 22:33:14 +03:00
link2xt
d179dced4e Get rid of mod.rs files.
Since Rust 2018 [1] it is no longer required to move module code to
`mod.rs` when submodules are added. This eliminates common problem of
having multiple buffers named `mod.rs` in editor.

[1] https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html#no-more-modrs
2021-04-23 22:45:45 +03:00
link2xt
1dc055fb66 cargo update -p sqlx 2021-04-23 00:00:00 +00:00
Hocuri
819775ac39 Revert "More logging for "core spams imap events""
This reverts commit 5394327bf6.
2021-04-23 09:15:15 +02:00
Hocuri
a1ef32170d Make logging less verbose 2021-04-22 18:03:37 +02:00
Hocuri
a4486d8c30 Fix #2335 (delete job flooding) (#2372)
* Fix #2335 (delete job flooding)

The problem was:

- You are offline, and an ephemeral message is due to delete.
- load_imap_deletion_job() is called and returns a deletion job for it.
- The job fails because, well, we are offline.
- The job is saved into the database.
- load_imap_deletion_job() is called again, and so on.

* Add test
2021-04-22 16:37:22 +02:00
link2xt
7bdae8b2c5 README.md: replace shields.io with official CircleCI badge
Also remove appveyor, it is not used anymore.
2021-04-21 07:23:48 +03:00
link2xt
75999c5d5a test_account.py: fix syntax error on python 3.5
It was introduced in 553d3936a9
2021-04-21 02:49:20 +03:00
B. Petersen
34ffa4e7ea add a test to check LIMIT on global searches 2021-04-20 13:51:49 +02:00
B. Petersen
3f1623eab1 LIMIT global search 2021-04-20 13:51:49 +02:00
link2xt
99373774aa search_msgs: do not match contact names
ct.name was insufficient, as authname, overridden name and email address
fallback were not taken into account.

Dropping this condition increases performance by 25% according to the
benchmark.

Also add a test for search_msgs.
2021-04-20 12:21:04 +02:00
link2xt
acd51a7058 Sort global message search result only by ID
It reduces the time by ~20% according to `search_msgs` benchmark.

Sorting by IDs is sufficient for global search as IDs increase in the
order of message reception.
2021-04-20 00:35:21 +03:00
B. Petersen
61bf0b208c add some tests for constants 2021-04-19 23:09:27 +03:00
link2xt
efd0314872 dc_receive_imf: remove unnecessary check for empty folder name
This check dates back to C core, where it checked for NULL, not empty string.
2021-04-19 23:09:17 +03:00
link2xt
ef89bc64c9 Add search_msgs benchmark 2021-04-19 23:09:00 +03:00
link2xt
6d4ec75a7b Benchmark reading contact list 2021-04-19 23:09:00 +03:00
link2xt
8af47de5a4 Benchmark adding 500 contacts from address book 2021-04-19 23:09:00 +03:00
link2xt
c7345c16f8 README: update CI status badges 2021-04-19 01:23:59 +03:00
link2xt
a4b14c6b98 ci: update configs to use scripts from scripts/ directory 2021-04-18 21:57:04 +03:00
link2xt
321354531d Move set_core_version.py into scripts/
Also make it executable.
2021-04-18 21:57:04 +03:00
link2xt
5b0f07f9a7 Move run-integration-tests.sh into scripts/ 2021-04-18 21:57:04 +03:00
link2xt
87cb5de8b1 Rename ci_scripts/ into scripts/ 2021-04-18 21:57:04 +03:00
link2xt
baae31117f server_params.rs: increase test coverage 2021-04-18 21:56:10 +03:00
Hocuri
553d3936a9 Some general Python test improvements (#2316)
This PR originally contained a fix for sqlx which turned out not not to be necessary. But on the way, I made some general improvements:

- Under some circumstances, a "normal" test failure led to a timeout, without printing a decent error message. See e.g. https://app.circleci.com/pipelines/github/deltachat/deltachat-core-rust/8069/workflows/ba2a9949-b4ad-4ceb-a930-073bba05e2db/jobs/30965.
  (The problem was: if there is an exception in dc_account_extra_configure(), when trying to handle the exception the line account.log("===================", e, "===================") was called, which can't work as log() only expects one parameter)
- When a test fails: Call `dump_account_info()` even if there is no direct_imap
- In test_import_export_online_all(), add another 100KB file to the backup. This adds resilience against future size changes of the sql db file: The test tests the smoothness of the progress bar. And if there are there are not enough about-equally-sized files, the progress bar can't be smooth.
2021-04-18 19:20:31 +02:00
link2xt
004fb76864 Remove println! calls from test_group_with_removed_message_id()
They were accidentally added in 6bb5721f29

Given that they are full of typos, they were probably not meant to be commited.
2021-04-18 18:37:49 +03:00
link2xt
09bc8fc603 dc_tools: remove dead code from the test
Since temporary directory is used, files from previous tests can't exist in blobdir.
2021-04-18 02:57:17 +03:00
link2xt
8f1bb38a3b Fix a comment typo 2021-04-18 02:56:34 +03:00
B. Petersen
2688f233b8 add timeinfo for 'listmsgs' repl command 2021-04-18 00:51:51 +02:00
B. Petersen
7be8fb7245 bumb version to 1.53.0 2021-04-18 00:51:51 +02:00
B. Petersen
a9b8776342 update changelog for 1.53.0 2021-04-18 00:51:51 +02:00
link2xt
9a34fe5c70 sql: enable shared cache 2021-04-17 23:47:21 +03:00
link2xt
e35a8d4415 sql: use sqlite3_last_insert_rowid instead of SELECT 2021-04-17 22:59:34 +03:00
Asiel Díaz Benítez
59dea29e88 Merge pull request #2333 from deltachat/adb-issue-2328
Add html API
2021-04-17 13:55:48 -04:00
Asiel Díaz Benítez
cfdc841c7e Update python/tests/test_account.py
Co-authored-by: Hocuri <hocuri@gmx.de>
2021-04-17 12:45:50 -04:00
Asiel Díaz Benítez
2974affaeb Merge pull request #2334 from deltachat/adb-issue-2329
Add "override_sender_name" API
2021-04-16 15:23:31 -04:00
link2xt
69dae4c006 Switch to release version of sqlx 2021-04-16 22:05:21 +03:00
link2xt
a795ae98ee Test saving and loading of LoginParam 2021-04-16 21:47:39 +03:00
link2xt
ac54301923 mimefactory: resultify is_file_size_okay 2021-04-16 21:47:29 +03:00
B. Petersen
9ecb6d9b15 test dehtml for pre-tag (wrote that little test to test the new coverage script :)[D 2021-04-15 01:49:12 +03:00
link2xt
ac9394cb16 dehtml.rs: test </i> tag 2021-04-15 00:30:50 +03:00
link2xt
edb9ea0e83 format_flowed.rs: increase line coverage to 100% 2021-04-15 00:30:50 +03:00
link2xt
4c1315446e Add coverage script 2021-04-15 00:30:50 +03:00
B. Petersen
cc6b02f037 remove additional tags: check from ci, it is 'master branch || has tags' so that does not make much sense, generation should be done on master only 2021-04-14 16:55:45 +02:00
B. Petersen
e13bb8fbd4 bump version to 1.52.0 2021-04-14 16:55:45 +02:00
B. Petersen
eaca4446aa update changelog to 1.52 2021-04-14 16:55:45 +02:00
B. Petersen
f3fb26c066 add a test to search for one-to-one-chats coming without authnames 2021-04-14 14:09:35 +02:00
B. Petersen
1a1416e446 change chat names correctly on contact name change
the user-given contact name may be set to an empty string;
in this case the authname or the email is used for the contact
and also for the name of possibly existing chats.

this works well for `dc_chat_get_name()` as that just uses
`dc_contact_get_display_name()` for single-chats.

it did not work for `dc_get_chatlist(query)` as that
uses the database for performance reasons -
however, in the database, the empty string is written
instead of the display name is written for a chat.

this is fixed by this pr by also using
dc_contact_get_display_name() when updating the chats-table
(similar to `dc_create_chat_by_contact_id()`)
2021-04-14 14:09:35 +02:00
B. Petersen
0afc07f6e7 add a test to search for one-to-one-chats 2021-04-14 14:09:35 +02:00
adbenitez
e6d2b1052c fix typo 2021-04-13 04:23:36 -04:00
adbenitez
19c1e6efc3 try to fix test 2021-04-13 04:11:15 -04:00
adbenitez
26d9addc5d improve test, test has_html() 2021-04-13 04:11:15 -04:00
adbenitez
6601015a09 add test_html_message() 2021-04-13 04:11:15 -04:00
adbenitez
36653928f7 add html API 2021-04-13 04:11:15 -04:00
Asiel Díaz Benítez
dda3c605c6 Merge pull request #2326 from deltachat/adb-fix-message-id
make Message.id a dynamic property
2021-04-13 04:08:50 -04:00
adbenitez
2e015e685f add aditional check 2021-04-13 03:53:37 -04:00
adbenitez
d4a1858d41 try to fix test 2021-04-13 03:53:21 -04:00
adbenitez
d6a6ba01e4 fix test 2021-04-13 02:38:42 -04:00
adbenitez
27714f596e add test_message_override_sender_name() 2021-04-13 02:14:53 -04:00
link2xt
d4e065ee84 Update once_cell, base64, itertools, strum and strum_macros 2021-04-13 02:18:15 +03:00
link2xt
bc222af661 Cargo.toml: sort dependencies alphabetically
This is what we do in rPGP too.
2021-04-13 02:18:15 +03:00
adbenitez
f6136f0ecc fix Chat.prepare_message() 2021-04-12 14:59:15 -04:00
adbenitez
b2517d3060 make Message.id a property 2021-04-12 14:59:15 -04:00
link2xt
244260a978 Fix nightly clippy and rustc errors 2021-04-12 21:33:52 +03:00
adbenitez
dc6fb7d481 add "override_sender_name" API 2021-04-11 21:18:46 -04:00
Asiel Díaz Benítez
f17320a9cb Merge pull request #2332 from deltachat/adb-issue-2327
Add sticker viewtype
2021-04-11 20:49:49 -04:00
adbenitez
d1237c9f8d add Message.is_sticker() 2021-04-11 20:37:47 -04:00
adbenitez
b9beaee7d4 update Message.new_empty() to allow view_type="sticker" and also allow using
message type directly (ex. `const.DC_MSG_STICKER`) so if new message types are
added, they can be used direcly without needing the python API to be updated.
2021-04-11 20:37:47 -04:00
adbenitez
258856c23a add sticker type 2021-04-11 20:37:47 -04:00
adbenitez
72ddd33adf avoid for loop 2021-04-11 20:37:47 -04:00
link2xt
1cd53aafff Add support for "Mixed Up" MIME format
This is an PGP/MIME format produced by Microsoft Exchange and ProtonMail IMAP/SMTP Bridge,
described in detail in https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html

This patch adds seamless support for "Mixed Up" Encryption, repairing
mangled Autocrypt messages without notifying the user.
2021-04-11 19:50:59 +03:00
link2xt
4d2ac5a3a2 ci: switch to v2 of actions/checkout 2021-04-11 14:45:27 +03:00
link2xt
146db48c35 ci: use DCC_NEW_TMP_EMAIL for remote python tests 2021-04-11 14:45:27 +03:00
link2xt
9529d76d82 ci: update ci_scripts README 2021-04-11 14:45:27 +03:00
link2xt
b5f2752e41 python: remove DCC_PY_LIVECONFIG references from all scripts
This variable is not used anymore.
2021-04-11 14:45:27 +03:00
link2xt
ce4675e9f7 ci: move remote python tests from CircleCI to GitHub Actions 2021-04-11 14:45:27 +03:00
link2xt
f0bd129636 ci: fix syntax of git --format in run-doxygen.sh
git version 2.31.0 throws fatal error on --format without "="
2021-04-11 14:45:27 +03:00
link2xt
dfe3cabb14 circleci: remove remote_tests_rust
Rust tests are already running on GitHub Actions, this is duplicate work.
2021-04-11 14:45:27 +03:00
link2xt
09735b808e circleci: remove unused jobs 2021-04-11 14:45:27 +03:00
link2xt
37f68459f6 sql: make all queries persistent and update to upstream sqlx
&str queries are not persistent by default.  To make queries persistent,
they have to be constructed with sqlx::query.

Upstream sqlx does not contain the change that make all queries
persistent, but it is not needed anymore. but
2021-04-10 22:24:12 +02:00
Hocuri
3707471266 Add alias support 2 (#2297)
fix  #2073
fix #2292 (I think)

- Messages can be assigned to any chat by the References and In-Reply-To, also to 1:1 chats; this has higher priority than the group id because with ad-hoc groups, it can happen that two devices have different group ids for the same conversation thread.
- If `From` is not in the chat (we call this "shadow sender"), `OverrideSenderDisplayname` is set. This communicates to the UI that:
  - A `~`should be added in front of the sender's displayname.
  - Also in 1:1 chats, the sender's displayname and avatar is shown, as if this was a group.

  The "Unknown sender for this chat" messages are completely removed for unprotected groups.

For protected chats, everything stays as it was before.

POSTPONED:

- Maybe (if it turns out to be still necessary):
  - The ad-hoc group id is computed by the the References, instead of the member list, as it is currently done
  - How do we prevent that the message can't be assigned to the correct chat as the parent message was deleted?
2021-04-10 22:06:22 +02:00
Hocuri
5394327bf6 More logging for "core spams imap events"
TODO: revert
2021-04-10 17:08:41 +03:00
Hocuri
df277b374d Ignore unknown classical emails from spam folder (#2311) 2021-04-10 10:45:47 +02:00
link2xt
53dba3c1ba Merge in sqlx fixes 2021-04-08 22:54:58 +03:00
link2xt
6540ee60e5 Update Cargo.lock 2021-04-08 22:05:15 +03:00
link2xt
66b5084a1d Switch to /deltachat/ org fork of sqlx 2021-04-08 21:58:04 +03:00
link2xt
f76aaf3205 sql: enable virtual statement cache on the reader pool
A follow-up to 720135a915
2021-04-07 21:43:34 +03:00
Hocuri
179a2a50e6 Parse <blockquote> tags for better quote detection (#2313) 2021-04-07 18:45:00 +02:00
link2xt
720135a915 Update sqlx to enable statement cache 2021-04-07 12:41:23 +03:00
Friedel Ziegelmayer
6bb5721f29 feat: improve internal sql interface
Switches from rusqlite to sqlx to have a fully async based interface
to sqlite.

Co-authored-by: B. Petersen <r10s@b44t.com>
Co-authored-by: Hocuri <hocuri@gmx.de>
Co-authored-by: link2xt <link2xt@testrun.org>
2021-04-06 16:06:11 +02:00
link2xt
4dedc2d8ce Fix a comment typo 2021-03-27 21:11:34 +03:00
link2xt
ede9bdc018 Reduce required cmake version to 3.16 2021-03-27 00:17:04 +03:00
holger krekel
11823d3b45 use master for tag-buids of upload wheels job 2021-03-23 22:20:14 +01:00
missytake
734ea8ab1b Merge pull request #2314 from deltachat/py51release
prepare 1.51.0 release
2021-03-23 19:42:39 +01:00
holger krekel
7017a050cb prepare 1.51.0 release 2021-03-23 18:55:34 +01:00
B. Petersen
96e57e7ef3 bump version to 1.51.0 2021-03-23 18:51:26 +01:00
B. Petersen
02bc334af5 update changelog for 1.51 2021-03-23 18:51:26 +01:00
Simon Laux
c8fea9c577 Merge pull request #2303 from deltachat/add_cmake_build_to_gitignore
add /build directory to .gitignore
2021-03-21 17:12:03 +01:00
link2xt
cdc1063d83 Do not reset user status after receiving a read receipt
Read receipts never contain the signature, so previously receiving it
cleared the status.
2021-03-21 18:54:08 +03:00
Simon Laux
704a902cc5 add build directory to gitignore
(libdeltachat generated with cmake)
2021-03-20 18:23:30 +01:00
B. Petersen
36aef6499d update provider database 2021-03-18 21:55:33 +01:00
B. Petersen
4ba9c2fafa fix clippy error on generating rust code from python 2021-03-18 21:55:33 +01:00
Hocuri
0de8b6a7e5 Update uid_next if the server rewinded it
fix #2188

Also, if we notice that the server started reusing old UIDs, _also_ do a `ResyncFolders`, because the server likely forgot to change uid_validity
2021-03-18 16:14:56 +03:00
link2xt
04f816be31 qr: return QrFprMismatch on fingerprint mismatch
Previously QrFprWithoutAddr was returned incorrectly.

Also fix spelling error ("Missmatch").
2021-03-15 21:39:10 +03:00
145 changed files with 13030 additions and 7759 deletions

View File

@@ -1,234 +0,0 @@
version: 2.1
executors:
default:
docker:
- image: filecoin/rust:latest
working_directory: /mnt/crate
doxygen:
docker:
- image: hrektts/doxygen
restore-workspace: &restore-workspace
attach_workspace:
at: /mnt
restore-cache: &restore-cache
restore_cache:
keys:
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- repo-source-{{ .Branch }}-{{ .Revision }}
commands:
test_target:
parameters:
target:
type: string
steps:
- *restore-workspace
- *restore-cache
- run:
name: Test (<< parameters.target >>)
command: TARGET=<< parameters.target >> ci_scripts/run-rust-test.sh
no_output_timeout: 15m
jobs:
cargo_fetch:
executor: default
steps:
- checkout
- restore_cache:
keys:
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- run: rustup install $(cat rust-toolchain)
- run: rustup default $(cat rust-toolchain)
- run: rustup component add --toolchain $(cat rust-toolchain) rustfmt
- run: rustup component add --toolchain $(cat rust-toolchain) clippy-preview
- run: cargo fetch
- run: rustc +stable --version
- run: rustc +$(cat rust-toolchain) --version
# make sure this git repo doesn't grow too big
- run: git gc
- persist_to_workspace:
root: /mnt
paths:
- crate
- save_cache:
key: cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
paths:
- "~/.cargo"
- "~/.rustup"
rustfmt:
executor: default
steps:
- *restore-workspace
- *restore-cache
- run:
name: Run cargo fmt
command: cargo fmt --all -- --check
test_macos:
macos:
xcode: "10.0.0"
working_directory: ~/crate
steps:
- run:
name: Configure environment variables
command: |
echo 'export PATH="${HOME}/.cargo/bin:${HOME}/.bin:${PATH}"' >> $BASH_ENV
echo 'export CIRCLE_ARTIFACTS="/tmp"' >> $BASH_ENV
- checkout
- run:
name: Install Rust
command: |
curl https://sh.rustup.rs -sSf | sh -s -- -y
- run: rustup install $(cat rust-toolchain)
- run: rustup default $(cat rust-toolchain)
- run: cargo fetch
- run:
name: Test
command: TARGET=x86_64-apple-darwin ci_scripts/run-rust-test.sh
test_x86_64-unknown-linux-gnu:
executor: default
steps:
- test_target:
target: "x86_64-unknown-linux-gnu"
test_i686-unknown-linux-gnu:
executor: default
steps:
- test_target:
target: "i686-unknown-linux-gnu"
test_aarch64-linux-android:
executor: default
steps:
- test_target:
target: "aarch64-linux-android"
build_doxygen:
executor: doxygen
steps:
- checkout
- run: bash ci_scripts/run-doxygen.sh
- run: mkdir -p workspace/c-docs
- run: cp -av deltachat-ffi/{html,xml} workspace/c-docs/
- persist_to_workspace:
root: workspace
paths:
- c-docs
remote_python_packaging:
machine: true
steps:
- checkout
# the following commands on success produces
# workspace/{wheelhouse,py-docs} as artefact directories
- run: bash ci_scripts/remote_python_packaging.sh
- persist_to_workspace:
root: workspace
paths:
# - c-docs
- py-docs
- wheelhouse
remote_tests_rust:
machine: true
steps:
- checkout
- run: ci_scripts/remote_tests_rust.sh
remote_tests_python:
machine: true
steps:
- checkout
- run: ci_scripts/remote_tests_python.sh
upload_docs_wheels:
machine: true
steps:
- checkout
- attach_workspace:
at: workspace
- run: ls -laR workspace
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
clippy:
executor: default
steps:
- *restore-workspace
- *restore-cache
- run:
name: Run cargo clippy
command: cargo clippy
workflows:
version: 2.1
test:
jobs:
# - cargo_fetch
- remote_tests_rust:
filters:
tags:
only: /.*/
- remote_tests_python:
filters:
tags:
only: /.*/
- remote_python_packaging:
filters:
branches:
only: master
#tags:
# only: /.*/
- upload_docs_wheels:
requires:
- remote_python_packaging
- build_doxygen
filters:
branches:
only: master
tags:
only: /.*/
# - rustfmt:
# requires:
# - cargo_fetch
# - clippy:
# requires:
# - cargo_fetch
- build_doxygen:
filters:
branches:
only: master
tags:
only: /.*/
# Linux Desktop 64bit
# - test_x86_64-unknown-linux-gnu:
# requires:
# - cargo_fetch
# Linux Desktop 32bit
# - test_i686-unknown-linux-gnu:
# requires:
# - cargo_fetch
# Android 64bit
# - test_aarch64-linux-android:
# requires:
# - cargo_fetch
# Desktop Apple
# - test_macos:
# requires:
# - cargo_fetch

9
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: "cargo"
open-pull-requests-limit: 10

View File

@@ -14,11 +14,11 @@ jobs:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.50.0
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
@@ -29,10 +29,10 @@ jobs:
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.50.0
toolchain: stable
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
@@ -65,26 +65,27 @@ jobs:
build_and_test:
name: Build and test
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
# macOS disabled due to random failures related to caching
#os: [ubuntu-latest, windows-latest, macOS-latest]
os: [ubuntu-latest, windows-latest]
rust: [1.50.0]
experimental: [false]
# include:
# - os: ubuntu-latest
# rust: nightly
# experimental: true
# - os: windows-latest
# rust: nightly
# experimental: true
# - os: macOS-latest
# rust: nightly
# experimental: true
include:
# Currently used Rust version, same as in `rust-toolchain` file.
- os: ubuntu-latest
rust: 1.54.0
python: 3.9
- os: windows-latest
rust: 1.54.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.48.0
# This is the Debian "bullseye" release version of Rust.
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.48.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@master
@@ -114,8 +115,10 @@ jobs:
- name: check
uses: actions-rs/cargo@v1
env:
RUSTFLAGS: -D warnings
with:
command: check
command: check
args: --all --bins --examples --tests --features repl
- name: tests
@@ -123,3 +126,29 @@ jobs:
with:
command: test
args: --all
- name: install python
if: ${{ matrix.python }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python }}
- name: install tox
if: ${{ matrix.python }}
run: pip install tox
- name: build C library
if: ${{ matrix.python }}
uses: actions-rs/cargo@v1
with:
command: build
args: -p deltachat_ffi
- name: run python tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e lint,doc,py3

32
.github/workflows/repl.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
# Manually triggered action to build a Windows repl.exe which users can
# download to debug complex bugs.
name: Build Windows REPL .exe
on:
workflow_dispatch:
jobs:
build_repl:
name: Build REPL example
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: 1.50.0
override: true
- name: build
uses: actions-rs/cargo@v1
with:
command: build
args: --example repl --features repl,vendored
- name: Upload binary
uses: actions/upload-artifact@v2
with:
name: repl.exe
path: 'target/debug/examples/repl.exe'

3
.gitignore vendored
View File

@@ -1,5 +1,6 @@
/target
**/*.rs.bk
/build
# ignore vi temporaries
*~
@@ -25,3 +26,5 @@ deltachat-ffi/html
deltachat-ffi/xml
.rsynclist
coverage/

View File

@@ -1,22 +1,253 @@
# Changelog
## UNRELEASED
## 1.59.0
- breaking change: You have to call dc_stop_io()/dc_start_io() before/after EXPORT_BACKUP:
fix race condition and db corruption when a message was received during backup #2253
### Added
- add quota information to `dc_get_connectivity_html()`
- save subject for messages:
new api `dc_msg_get_subject()`,
when quoting, use the subject of the quoted message as the new subject, instead of the
last subject in the chat
### Changes
- refactorings #2592 #2570 #2581
- add 'device chat about' to now existing status #2613
- update provider database #2608
### Fixes
- provider database supports socket=PLAIN and dotless domains now #2604 #2608
- add migrated accounts to events emitter #2607
- fix forwarding quote-only mails #2600
- do not set WantsMdn param for outgoing messages #2603
- set timestamps for system messages #2593
- do not treat gmail labels as folders #2587
- avoid timing problems in `dc_maybe_network_lost()` #2551
- only set smtp to "connected" if the last message was actually sent #2541
## 1.58.0
### Fixes
- move WAL file together with database
and avoid using data if the database was not closed correctly before #2583
## 1.57.0
### API Changes
- breaking change: removed deaddrop chat #2514 #2563
Contact request chats are not merged into a single virtual
"deaddrop" chat anymore. Instead, they are shown in the chatlist the
same way as other chats, but sending of messages to them is not
allowed and MDNs are not sent automatically until the chat is
"accepted" by the user.
New API:
- `dc_chat_is_contact_request()`: returns true if chat is a contact
request. In this case an option to accept the chat via
`dc_accept_chat()` should be shown in the UI.
- `dc_accept_chat()`: unblock the chat or accept contact request
- `dc_block_chat()`: block the chat, currently works only for mailing
lists.
Removed API:
- `dc_create_chat_by_msg_id()`: deprecated 2021-02-07 in favor of
`dc_decide_on_contact_request()`
- `dc_marknoticed_contact()`: deprecated 2021-02-07 in favor of
`dc_decide_on_contact_request()`
- `dc_decide_on_contact_request()`: this call requires a message ID
from deaddrop chat as input. As deaddrop chat is removed, this
call can't be used anymore.
- `dc_msg_get_real_chat_id()`: use `dc_msg_get_chat_id()` instead, the
only difference between these calls was in handling of deaddrop
chat
- removed `DC_CHAT_ID_DEADDROP` and `DC_STR_DEADDROP` constants
- breaking change: removed `DC_EVENT_ERROR_NETWORK` and `DC_STR_SERVER_RESPONSE`
Instead, there is a new api `dc_get_connectivity()`
and `dc_get_connectivity_html()`;
`DC_EVENT_CONNECTIVITY_CHANGED` is emitted on changes
- breaking change: removed `dc_accounts_import_account()`
Instead you need to add an account and call `dc_imex(DC_IMEX_IMPORT_BACKUP)`
on its context
- update account api, 2 new methods:
`int dc_all_work_done (dc_context_t* context);`
`int dc_accounts_all_work_done (dc_accounts_t* accounts);`
- add api to check if a message was `Auto-Submitted`
cffi: `int dc_msg_is_bot (const dc_msg_t* msg);`
python: `Message.is_bot()`
- `dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);`
now returns `NULL` if there is no selected account
- added `dc_accounts_maybe_network_lost()` for systems core cannot find out
connectivity loss on its own (eg. iOS) #2550
### Added
- use Auto-Submitted: auto-generated header to identify bots #2502
- allow sending stickers via repl tool
- chat: make `get_msg_cnt()` and `get_fresh_msg_cnt()` work for deaddrop chat #2493
- withdraw/revive own qr-codes #2512
- add Connectivity view (a better api for getting the connection status) #2319 #2549 #2542
### Changes
- updated spec: new `Chat-User-Avatar` usage, `Chat-Content: sticker`, structure, copyright year #2480
- update documentation #2548 #2561 #2569
- breaking: `Accounts::create` does not also create an default account anymore #2500
- remove "forwarded" from stickers, as the primary way of getting stickers
is by asking a bot and then forwarding them currently #2526
- mimeparser: use mailparse to parse RFC 2231 filenames #2543
- allow email addresses without dot in the domain part #2112
- allow installing lib and include under different prefixes #2558
- remove counter from name provided by `DC_CHAT_ID_ARCHIVED_LINK` #2566
- improve tests #2487 #2491 #2497
- refactorings #2492 #2503 #2504 #2506 #2515 #2520 #2567 #2575 #2577 #2579
- improve ci #2494
- update provider-database #2565
### Removed
- remove `dc_accounts_import_account()` api #2521
- remove `DC_EVENT_ERROR_NETWORK` and `DC_STR_SERVER_RESPONSE` #2319
### Fixes
- allow stickers with gif-images #2481
- fix database migration #2486
- do not count hidden messages in get_msg_cnt(). #2493
- improve drafts detection #2489
- fix panic when removing last, selected account from account manager #2500
- set_draft's message-changed-event returns now draft's msg id instead of 0 #2304
- avoid hiding outgoing classic emails #2505
- fixes for message timestamps #2517
- do not process names, avatars, location XMLs, message signature etc.
for duplicate messages #2513
- fix `can_send` for users not in group #2479
- fix receiving events for accounts added by `dc_accounts_add_account()` #2559
- fix which chats messages are assigned to #2465
- fix: don't create chats when MDNs are received #2578
## 1.56.0
- fix downscaling images #2469
- fix outgoing messages popping up in selfchat #2456
- securejoin: display error reason if there is any #2470
- do not allow deleting contacts with ongoing chats #2458
- fix: ignore drafts folder when scanning #2454
- fix: scan folders also when inbox is not watched #2446
- more robust In-Reply-To parsing #2182
- update dependencies #2441 #2438 #2439 #2440 #2447 #2448 #2449 #2452 #2453 #2460 #2464 #2466
- update provider-database #2471
- refactorings #2459 #2457
- improve tests and ci #2445 #2450 #2451
## 1.55.0
- fix panic when receiving some HTML messages #2434
- fix downloading some messages multiple times #2430
- fix formatting of read receipt texts #2431
- simplify SQL error handling #2415
- explicit rust API for creating chats with blocked status #2282
- debloat the binary by using less AsRef arguments #2425
## 1.54.0
- switch back from `sqlx` to `rusqlite` due to performance regressions #2380 #2381 #2385 #2387
- global search performance improvement #2364 #2365 #2366
- improve SQLite performance with `PRAGMA synchronous=normal` #2382
- python: fix building of bindings against system-wide install of `libdeltachat` #2383 #2385
- python: list `requests` as a requirement #2390
- fix creation of many delete jobs when being offline #2372
- synchronize status between devices #2386
- deaddrop (contact requests) chat improvements #2373
- add "Forwarded:" to notification and chatlist summaries #2310
- place user avatar directly into `Chat-User-Avatar` header #2232 #2384
- improve tests #2360 #2362 #2370 #2377 #2387
- cleanup #2359 #2361 #2374 #2376 #2379 #2388
## 1.53.0
- fix sqlx performance regression #2355 2356
- add a `ci_scripts/coverage.sh` #2333 #2334
- refactorings and tests #2348 #2349 #2350
- improve python bindings #2332 #2326
## 1.52.0
- database library changed from rusqlite to sqlx #2089 #2331 #2336 #2340
- add alias support: UIs should check for `dc_msg_get_override_sender_name()`
also in single-chats now and display divergent names and avatars #2297
- parse blockquote-tags for better quote detection #2313
- ignore unknown classical emails from spam folder #2311
- support "Mixed Up” encryption repairing #2321
- fix single chat search #2344
- fix nightly clippy and rustc errors #2341
- update dependencies #2350
- improve ci #2342
- improve python bindings #2332 #2326
## 1.51.0
- breaking change: You have to call `dc_stop_io()`/`dc_start_io()`
before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`:
fix race condition and db corruption
when a message was received during backup #2253
- save subject for messages: new api `dc_msg_get_subject()`,
when quoting, use the subject of the quoted message as the new subject,
instead of the last subject in the chat #2274 #2283
- new apis to get full or html message,
`dc_msg_has_html()` and `dc_get_msg_html()` #2125 #2151
`dc_msg_has_html()` and `dc_get_msg_html()` #2125 #2151 #2264 #2279
- new chat type and apis for the new mailing list support,
`DC_CHAT_TYPE_MAILINGLIST`, `dc_msg_get_real_chat_id()`,
`dc_msg_get_override_sender_name()` #1964 #2181 #2185 #2195 #2211 #2210 #2240
#2243
#2241 #2243 #2258 #2259 #2261 #2267 #2270 #2272 #2290
- new api `dc_decide_on_contact_request()`,
deprecated `dc_create_chat_by_msg_id()` and `dc_marknoticed_contact()` #1964
@@ -25,7 +256,7 @@
- new api `dc_get_chat_encrinfo()` #2186
- new api `dc_contact_get_status()`, returning the recent footer #2218
- new api `dc_contact_get_status()`, returning the recent footer #2218 #2307
- improve contact name update rules,
add api `dc_contact_get_auth_name()` #2206 #2212 #2225
@@ -38,6 +269,11 @@
- api removed: `dc_contact_get_first_name()` #2165 #2171
- improve compatibility with providers changing the Message-ID
(as Outlook.com) #2250 #2265
- correctly show emails that were sent to an alias and then bounced
- implement Consistent Color Generation (XEP-0392),
that results in contact colors be be changed #2228 #2229 #2239
@@ -72,7 +308,7 @@
- enable strict TLS for known providers by default #2121
- improve and harden secure join #2154 #2161
- improve and harden secure join #2154 #2161 #2251
- update `dc_get_info()` to return more information #2156
@@ -100,10 +336,24 @@
- fix parsing quoted encoded words in From: header #2193 #2204
- fix ci #2217 #2226
- fix import/export race condition #2250
- fix: exclude muted chats from notified-list #2269 #2275
- fix: update uid_next if the server rewind it #2288
- fix: return error on fingerprint mismatch on qr-scan #2295
- fix ci #2217 #2226 #2244 #2245 #2249 #2277 #2286
- try harder on backup opening #2148
- trash messages more thoroughly #2273
- nicer logging #2284
- add CMakeLists.txt #2260
- switch to rust 1.50, update toolchains, deps #2150 #2155 #2165 #2107 #2262 #2271
- improve python bindings #2113 #2115 #2133 #2214
@@ -111,7 +361,9 @@
- improve documentation #2143 #2160 #2175 #2146
- refactorings #2110 #2136 #2135 #2168 #2178 #2189 #2190 #2198 #2197 #2201 #2196
#2200 #2230
#2200 #2230 #2262 #2203
- update provider-database #2299
## 1.50.0

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.18)
project (deltachat)
cmake_minimum_required(VERSION 3.16)
project(deltachat LANGUAGES C)
find_program(CARGO cargo)
@@ -8,8 +8,22 @@ add_custom_command(
"target/release/libdeltachat.a"
"target/release/libdeltachat.so"
"target/release/pkgconfig/deltachat.pc"
COMMAND PREFIX=${CMAKE_INSTALL_PREFIX} ${CARGO} build --package deltachat_ffi --release
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --release --no-default-features
# Build in `deltachat-ffi` directory instead of using
# `--package deltachat_ffi` to avoid feature resolver version
# "1" bug which makes `--no-default-features` affect only
# `deltachat`, but not `deltachat-ffi` package.
#
# We can't enable version "2" resolver [1] because it is not
# stable yet on rust 1.50.0.
#
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
)
add_custom_target(

1063
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.51.0-alpha.0"
version = "1.59.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -14,76 +14,76 @@ lto = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { version = "0.7.0", default-features = false }
ansi_term = { version = "0.12.1", optional = true }
anyhow = "1.0.42"
async-imap = { git = "https://github.com/async-email/async-imap" }
async-native-tls = { version = "0.3.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="c8800625f7cf29f437143ac7e720ac2730a0962f" }
async-std-resolver = "0.20.3"
async-std = { version = "~1.9.0", features = ["unstable"] }
async-tar = "0.3.0"
async-trait = "0.1.50"
backtrace = "0.3.59"
base64 = "0.13"
bitflags = "1.3.1"
byteorder = "1.3.1"
chrono = "0.4.6"
dirs = { version = "3.0.2", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
escaper = "0.1.1"
futures = "0.3.16"
hex = "0.4.0"
sha-1 = "0.9.3"
sha2 = "0.9.0"
rand = "0.7.0"
smallvec = "1.0.0"
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
indexmap = "1.7.0"
itertools = "0.10.1"
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2.98"
log = {version = "0.4.8", optional = true }
mailparse = "0.13.5"
native-tls = "0.2.3"
num_cpus = "1.13.0"
num-derive = "0.3.0"
num-traits = "0.2.6"
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
async-imap = "0.4.0"
async-native-tls = { version = "0.3.3" }
async-std = { version = "~1.8.0", features = ["unstable"] }
base64 = "0.12"
charset = "0.1"
once_cell = "1.8.0"
percent-encoding = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4.6"
indexmap = "1.3.0"
kamadak-exif = "0.5"
once_cell = "1.4.1"
regex = "1.1.6"
rusqlite = { version = "0.24", features = ["bundled"] }
r2d2_sqlite = "0.17.0"
r2d2 = "0.8.5"
strum = "0.19.0"
strum_macros = "0.19.0"
backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.9.0"
quick-xml = "0.18.1"
escaper = "0.1.0"
bitflags = "1.1.0"
sanitize-filename = "0.3.0"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.13.0"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
futures = "0.3.4"
thiserror = "1.0.14"
anyhow = "1.0.28"
async-trait = "0.1.31"
url = "2.1.1"
async-std-resolver = "0.19.5"
async-tar = "0.3.0"
uuid = { version = "0.8", features = ["serde", "v4"] }
rust-hsluv = "0.1.4"
pgp = { version = "0.7.0", default-features = false }
pretty_env_logger = { version = "0.4.0", optional = true }
log = {version = "0.4.8", optional = true }
rustyline = { version = "4.1.0", optional = true }
ansi_term = { version = "0.12.1", optional = true }
dirs = { version = "3.0.1", optional=true }
quick-xml = "0.22.0"
r2d2 = "0.8.9"
r2d2_sqlite = "0.18.0"
rand = "0.7.0"
regex = "1.4.6"
rusqlite = "0.25"
rust-hsluv = "0.1.4"
rustyline = { version = "8.2.0", optional = true }
sanitize-filename = "0.3.0"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.9.7"
sha2 = "0.9.5"
smallvec = "1.0.0"
stop-token = "0.2.0"
strum = "0.21.0"
strum_macros = "0.21.1"
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
thiserror = "1.0.26"
toml = "0.5.6"
url = "2.2.2"
uuid = { version = "0.8", features = ["serde", "v4"] }
humansize = "1.1.1"
[dev-dependencies]
tempfile = "3.0"
pretty_assertions = "0.6.1"
pretty_env_logger = "0.4.0"
proptest = "0.10"
async-std = { version = "1.6.4", features = ["unstable", "attributes"] }
futures-lite = "1.7.0"
criterion = "0.3"
ansi_term = "0.12.0"
async-std = { version = "1.9.0", features = ["unstable", "attributes"] }
criterion = "0.3"
futures-lite = "1.12.0"
log = "0.4.11"
pretty_assertions = "0.7.2"
pretty_env_logger = "0.4.0"
proptest = "1.0"
tempfile = "3.0"
[workspace]
members = [
@@ -106,10 +106,17 @@ required-features = ["repl"]
name = "create_account"
harness = false
[[bench]]
name = "contacts"
harness = false
[[bench]]
name = "search_msgs"
harness = false
[features]
default = []
default = ["vendored"]
internals = []
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled"]
nightly = ["pgp/nightly"]

View File

@@ -2,7 +2,7 @@
> Deltachat-core written in Rust
[![CircleCI build status][circle-shield]][circle] [![Appveyor build status][appveyor-shield]][appveyor]
[![Rust CI](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
## Installing Rust and Cargo
@@ -17,7 +17,7 @@ $ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`:
```
$ RUST_LOG=info cargo run --example repl --features repl -- ~/deltachat-db
$ RUST_LOG=repl=info cargo run --example repl --features repl -- ~/deltachat-db
```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
@@ -79,6 +79,16 @@ For more commands type:
> help
```
## Installing libdeltachat system wide
```
$ git clone https://github.com/deltachat/deltachat-core-rust.git
$ cd deltachat-core-rust
$ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr
$ cmake --build build
$ sudo cmake --install build
```
## Development
```sh
@@ -95,7 +105,7 @@ $ cargo build -p deltachat_ffi --release
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
- `RUST_LOG=info,async_imap=trace,async_smtp=trace`: enable IMAP and
- `RUST_LOG=repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
SMTP tracing in addition to info messages.
### Expensive tests
@@ -111,11 +121,6 @@ $ cargo test -- --ignored
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
- `nightly`: Enable nightly only performance and security related features.
[circle-shield]: https://img.shields.io/circleci/project/github/deltachat/deltachat-core-rust/master.svg?style=flat-square
[circle]: https://circleci.com/gh/deltachat/deltachat-core-rust/
[appveyor-shield]: https://ci.appveyor.com/api/projects/status/lqpegel3ld4ipxj8/branch/master?style=flat-square
[appveyor]: https://ci.appveyor.com/project/dignifiedquire/deltachat-core-rust/branch/master
## Language bindings and frontend projects
Language bindings are available for:

39
benches/contacts.rs Normal file
View File

@@ -0,0 +1,39 @@
use async_std::task::block_on;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::contact::Contact;
use deltachat::context::Context;
use tempfile::tempdir;
async fn address_book_benchmark(n: u32, read_count: u32) {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 100;
let context = Context::new("FakeOS".into(), dbfile.into(), id)
.await
.unwrap();
let book = (0..n)
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
.collect::<Vec<String>>()
.join("");
Contact::add_address_book(&context, &book).await.unwrap();
let query: Option<&str> = None;
for _ in 0..read_count {
Contact::get_all(&context, 0, query).await.unwrap();
}
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("create 500 contacts", |b| {
b.iter(|| block_on(async { address_book_benchmark(black_box(500), black_box(0)).await }))
});
c.bench_function("create 100 contacts and read it 1000 times", |b| {
b.iter(|| block_on(async { address_book_benchmark(black_box(100), black_box(1000)).await }))
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

29
benches/search_msgs.rs Normal file
View File

@@ -0,0 +1,29 @@
use async_std::task::block_on;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use std::path::Path;
async fn search_benchmark(path: impl AsRef<Path>) {
let dbfile = path.as_ref();
let id = 100;
let context = Context::new("FakeOS".into(), dbfile.into(), id)
.await
.unwrap();
for _ in 0..10u32 {
context.search_msgs(None, "hello").await.unwrap();
}
}
fn criterion_benchmark(c: &mut Criterion) {
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
// messages, such as your primary account.
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
c.bench_function("search hello", |b| {
b.iter(|| block_on(async { search_benchmark(black_box(&path)).await }))
});
}
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -1,74 +0,0 @@
#!/bin/bash
if [ -z "$DEVPI_LOGIN" ] ; then
echo "required: password for 'dc' user on https://m.devpi/net/dc index"
exit 0
fi
set -xe
PYDOCDIR=${1:?directory with python docs}
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
DOXYDOCDIR=${3:?directory where doxygen docs to be found}
SSHTARGET=ci@b1.delta.chat
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}/wheelhouse
# python docs to py.delta.chat
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
rsync -avz \
--delete \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
"$PYDOCDIR/html/" \
delta@py.delta.chat:build/${BRANCH}
# C docs to c.delta.chat
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@c.delta.chat mkdir -p build-c/${BRANCH}
rsync -avz \
--delete \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
"$DOXYDOCDIR/html/" \
delta@c.delta.chat:build-c/${BRANCH}
echo -----------------------
echo upload wheels
echo -----------------------
# Bundle external shared libraries into the wheels
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $SSHTARGET mkdir -p $BUILDDIR
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ci_scripts/cleanup_devpi_indices.py $SSHTARGET:$BUILDDIR
rsync -avz \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
$WHEELHOUSEDIR \
$SSHTARGET:$BUILDDIR
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
# we rely on the "venv" virtualenv on the remote account to exist
source venv/bin/activate
cd $BUILDDIR
devpi use https://m.devpi.net
devpi login dc --password $DEVPI_LOGIN
N_BRANCH=${BRANCH//[\/]}
devpi use dc/\$N_BRANCH || {
devpi index -c \$N_BRANCH
devpi use dc/\$N_BRANCH
}
devpi index \$N_BRANCH bases=/root/pypi
devpi upload wheelhouse/deltachat*
# remove devpi non-master dc indices if thy are too old
# this script was copied above
python cleanup_devpi_indices.py
_HERE

View File

@@ -1,14 +0,0 @@
#!/bin/bash
set -x -e
# we use the python3.5 environment as the base environment
/opt/python/cp35-cp35m/bin/pip install tox devpi-client auditwheel
pushd /usr/bin
ln -s /opt/_internal/cpython-3.5.*/bin/tox
ln -s /opt/_internal/cpython-3.5.*/bin/devpi
ln -s /opt/_internal/cpython-3.5.*/bin/auditwheel
popd

View File

@@ -1,11 +0,0 @@
#!/bin/bash
set -e -x
# Install Rust
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.50.0-x86_64-unknown-linux-gnu -y
export PATH=/root/.cargo/bin:$PATH
rustc --version
# remove some 300-400 MB that we don't need for automated builds
rm -rf /root/.rustup/toolchains/1.50.0-x86_64-unknown-linux-gnu/share

View File

@@ -1,9 +0,0 @@
#!/bin/bash
set -xe
export CIRCLE_JOB=remote_tests_${1:?need to specify 'rust' or 'python'}
export CIRCLE_BUILD_NUM=$USER
export CIRCLE_BRANCH=`git branch | grep \* | cut -d ' ' -f2`
export CIRCLE_PROJECT_REPONAME=$(basename `git rev-parse --show-toplevel`)
time bash ci_scripts/$CIRCLE_JOB.sh

View File

@@ -1,77 +0,0 @@
name: CI
on:
pull_request:
push:
env:
RUSTFLAGS: -Dwarnings
jobs:
build_and_test:
name: Build and test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
rust: [nightly]
steps:
- uses: actions/checkout@master
- name: Install ${{ matrix.rust }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- name: check
uses: actions-rs/cargo@v1
if: matrix.rust == 'nightly'
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all
- name: tests ignored
uses: actions-rs/cargo@v1
with:
command: test
args: --all --release -- --ignored
check_fmt:
name: Checking fmt and docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
components: rustfmt
- name: fmt
run: cargo fmt --all -- --check
# clippy_check:
# name: Clippy check
# runs-on: ubuntu-latest
#
# steps:
# - uses: actions/checkout@v1
# - uses: actions-rs/toolchain@v1
# with:
# profile: minimal
# toolchain: nightly
# override: true
# components: clippy
#
# - name: clippy
# run: cargo clippy --all

View File

@@ -1,61 +0,0 @@
#!/bin/bash
#
# Build the Delta Chat C/Rust library typically run in a docker
# container that contains all library deps but should also work
# outside if you have the dependencies installed on your system.
set -e -x
# Perform clean build of core and install.
export TOXWORKDIR=.docker-tox
# install core lib
export PATH=/root/.cargo/bin:$PATH
cargo build --release -p deltachat_ffi
# cargo test --all --all-features
# Statically link against libdeltachat.a.
export DCC_RS_DEV=$(pwd)
# Configure access to a base python and to several python interpreters
# needed by tox below.
export PATH=$PATH:/opt/python/cp35-cp35m/bin
export PYTHONDONTWRITEBYTECODE=1
pushd /bin
ln -s /opt/python/cp27-cp27m/bin/python2.7
ln -s /opt/python/cp36-cp36m/bin/python3.6
ln -s /opt/python/cp37-cp37m/bin/python3.7
popd
if [ -n "$TESTS" ]; then
pushd python
# prepare a clean tox run
rm -rf tests/__pycache__
rm -rf src/deltachat/__pycache__
export PYTHONDONTWRITEBYTECODE=1
# run tox. The circle-ci project env-var-setting DCC_PY_LIVECONFIG
# allows running of "liveconfig" tests but for speed reasons
# we run them only for the highest python version we support
# we split out qr-tests run to minimize likelyness of flaky tests
# (some qr tests are pretty heavy in terms of send/received
# messages and rust's imap code likely has concurrency problems)
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
fi
# if [ -n "$DOCS" ]; then
# echo -----------------------
# echo generating python docs
# echo -----------------------
# (cd python && tox --workdir "$TOXWORKDIR" -e doc)
# fi

View File

@@ -1,50 +0,0 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:-master}
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
set -xe
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
git ls-files >.rsynclist
# we seem to need .git for setuptools_scm versioning
find .git >>.rsynclist
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
set +x
# we have to create a remote file for the remote-docker run
# so we can do a simple ssh command with a TTY
# so that when our job dies, all container-runs are aborted.
# sidenote: the circle-ci machinery will kill ongoing jobs
# if there are new commits and we want to ensure that
# everything is terminated/cleaned up and we have no orphaned
# useless still-running docker-containers consuming resources.
ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
set +x -e
shopt -s huponexit
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_NEW_TMP_EMAIL -e DCC_PY_LIVECONFIG \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh
_HERE
echo "--- Running $CIRCLE_JOB remotely"
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
mkdir -p workspace
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs

View File

@@ -1,30 +0,0 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:-master}
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
set -e
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
git ls-files >.rsynclist
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
echo "--- Running $CIRCLE_JOB remotely"
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
export TARGET=x86_64-unknown-linux-gnu
export RUSTC_WRAPPER=sccache
bash ci_scripts/run-rust-test.sh
_HERE

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -ex
cd deltachat-ffi
PROJECT_NUMBER=$(git log -1 --format "%h (%cd)") doxygen

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.51.0-alpha.0"
version = "1.59.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -20,9 +20,9 @@ libc = "0.2"
human-panic = "1.0.1"
num-traits = "0.2.6"
serde_json = "1.0"
async-std = "1.6.0"
anyhow = "1.0.28"
thiserror = "1.0.14"
async-std = "1.9.0"
anyhow = "1.0.42"
thiserror = "1.0.26"
rand = "0.7.3"
[features]

View File

@@ -23,11 +23,13 @@ fn main() {
version = env::var("CARGO_PKG_VERSION").unwrap(),
libs_priv = libs_priv,
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
);
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
fs::File::create(target_path.join("pkgconfig").join("deltachat.pc"))
.unwrap()
.write_all(&pkg_config.as_bytes())
.write_all(pkg_config.as_bytes())
.unwrap();
}

View File

@@ -305,7 +305,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
* also show all mails of confirmed contacts,
* DC_SHOW_EMAILS_ALL (2)=
* also show mails of unconfirmed contacts in the deaddrop.
* also show mails of unconfirmed contacts.
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
* generate recommended key type (default),
* DC_KEY_GEN_RSA2048 (1)=
@@ -340,7 +340,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
* The type `jitsi:` may be handled by external apps.
* If no type is prefixed, the videochat is handled completely in a browser.
* - `bot` = Set to "1" if this is a bot. E.g. prevents adding the "Device messages" and "Saved messages" chats.
* - `bot` = Set to "1" if this is a bot.
* Prevents adding the "Device messages" and "Saved messages" chats,
* adds Auto-Submitted header to outgoing messages.
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
@@ -461,11 +463,66 @@ char* dc_get_info (const dc_context_t* context);
char* dc_get_oauth2_url (dc_context_t* context, const char* addr, const char* redirect_uri);
#define DC_CONNECTIVITY_NOT_CONNECTED 1000
#define DC_CONNECTIVITY_CONNECTING 2000
#define DC_CONNECTIVITY_WORKING 3000
#define DC_CONNECTIVITY_CONNECTED 4000
/**
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
* One of:
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
*
* We don't use exact values but ranges here so that we can split up
* states into multiple states in the future.
*
* Meant as a rough overview that can be shown
* e.g. in the title of the main screen.
*
* If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*
* @memberof dc_context_t
* @param context The context object.
* @return The current connectivity.
*/
int dc_get_connectivity (dc_context_t* context);
/**
* Get an overview of the current connectivity, and possibly more statistics.
* Meant to give the user more insight about the current status than
* the basic connectivity info returned by dc_get_connectivity(); show this
* e.g., if the user taps on said basic connectivity info.
*
* If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*
* This comes as an HTML from the core so that we can easily improve it
* and the improvement instantly reaches all UIs.
*
* @memberof dc_context_t
* @param context The context object.
* @return An HTML page with some info about the current connectivity and status.
*/
char* dc_get_connectivity_html (dc_context_t* context);
/**
* Standalone version of dc_accounts_all_work_done().
* Only used by the python tests.
*/
int dc_all_work_done (dc_context_t* context);
// connect
/**
* Configure a context.
* During configuration IO must not be started, if needed stop IO using dc_stop_io() first.
* During configuration IO must not be started,
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
* If the context is already configured,
* this function will try to change the configuration.
*
@@ -625,12 +682,6 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
*
* By default, the function adds some special entries to the list.
* These special entries can be identified by the ID returned by dc_chatlist_get_chat_id():
* - DC_CHAT_ID_DEADDROP (1) - this special chat is present if there are
* messages from addresses that have no relationship to the configured account.
* The last of these messages is represented by DC_CHAT_ID_DEADDROP and you can retrieve details
* about it with dc_chatlist_get_msg_id(). Typically, the UI asks the user "Do you want to chat with NAME?"
* and offers the options "Start chat", "Block" or "Not now".
* Call dc_decide_on_contact_request() when the user selected one of these options.
* - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
* archived _any_ chat using dc_set_chat_visibility(). The UI should show a link as
* "Show archived chats", if the user clicks this item, the UI should show a
@@ -648,10 +699,10 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
* 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" and the deaddrop.
* and hides the "Device chat" and contact requests.
* typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
* to also hide the archive link.
* - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
* - if the flag DC_GCL_NO_SPECIALS is set, archive link is not added
* to the list (may be used e.g. for selecting chats on forwarding, the flag is
* not needed when DC_GCL_ARCHIVED_ONLY is already set)
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
@@ -671,42 +722,12 @@ dc_chatlist_t* dc_get_chatlist (dc_context_t* context, int flags,
// handle chats
/**
* Create a normal chat or a group chat by a messages ID that comes typically
* from the deaddrop, DC_CHAT_ID_DEADDROP (1).
*
* If the given message ID already belongs to a normal chat or to a group chat,
* the chat ID of this chat is returned and no new chat is created.
* If a new chat is created, the given message ID is moved to this chat, however,
* there may be more messages moved to the chat from the deaddrop. To get the
* chat messages, use dc_get_chat_msgs().
*
* If the user is asked before creation, he should be
* asked whether he wants to chat with the _contact_ belonging to the message;
* the group names may be really weird when taken from the subject of implicit
* groups and this may look confusing.
*
* Moreover, this function also scales up the origin of the contact belonging
* to the message and, depending on the contacts origin, messages from the
* same group may be shown or not - so, all in all, it is fine to show the
* contact name only.
*
* @deprecated Deprecated 2021-02-07, use dc_decide_on_contact_request() instead
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param msg_id The message ID to create the chat for.
* @return The created or reused chat ID on success. 0 on errors.
*/
uint32_t dc_create_chat_by_msg_id (dc_context_t* context, uint32_t msg_id);
/**
* Create a normal chat with a single user. To create group chats,
* see dc_create_group_chat().
*
* If a chat already exists, this ID is returned, otherwise a new chat is created;
* this new chat may already contain messages, e.g. from the deaddrop, to get the
* chat messages, use dc_get_chat_msgs().
* to get the chat messages, use dc_get_chat_msgs().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
@@ -1078,9 +1099,9 @@ int dc_estimate_deletion_cnt (dc_context_t* context, int from_ser
* or badge counters eg. on the app-icon.
* The list is already sorted and starts with the most recent fresh message.
*
* Messages belonging to muted chats are not returned,
* as they should not be notified
* and also a badge counters should not include messages of muted chats.
* Messages belonging to muted chats or to the contact requests are not returned;
* these messages should not be notified
* and also badge counters should not include these messages.
*
* To get the number of fresh messages for a single chat, muted or not,
* use dc_get_fresh_msg_cnt().
@@ -1213,6 +1234,31 @@ void dc_set_chat_visibility (dc_context_t* context, uint32_t ch
*/
void dc_delete_chat (dc_context_t* context, uint32_t chat_id);
/**
* Block a chat.
*
* Blocking 1:1 chats blocks the corresponding contact. Blocking
* mailing lists creates a pseudo-contact in the list of blocked
* contacts, so blocked mailing lists can be discovered and unblocked
* the same way as the contacts. Blocking group chats deletes the
* chat without blocking any contacts, so it may pop up again later.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to block.
*/
void dc_block_chat (dc_context_t* context, uint32_t chat_id);
/**
* Accept a contact request chat.
*
* Use it to accept "contact request" chats as indicated by dc_chat_is_contact_request().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to accept.
*/
void dc_accept_chat (dc_context_t* context, uint32_t chat_id);
/**
* Get contact IDs belonging to a chat.
@@ -1224,8 +1270,6 @@ void dc_delete_chat (dc_context_t* context, uint32_t ch
* explicitly as it may happen that oneself gets removed from a still existing
* group
*
* - for the deaddrop, the list is empty
*
* - for mailing lists, the behavior is not documented currently, we will decide on that later.
* for now, the UI should not show the list for mailing lists.
* (we do not know all members and there is not always a global mailing list address,
@@ -1272,6 +1316,12 @@ uint32_t dc_get_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id);
* search results may just hilite the corresponding messages and present a
* prev/next button.
*
* For global search, result is limited to 1000 messages,
* this allows incremental search done fast.
* So, when getting exactly 1000 results, the result may be truncated;
* the UIs may display sth. as "1000+ messages found" in this case.
* Chat search (if a chat_id is set) is not limited.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id ID of the chat to search messages in.
@@ -1568,32 +1618,22 @@ void dc_forward_msgs (dc_context_t* context, const uint3
/**
* Mark all messages sent by the given contact as _noticed_.
* This function is typically used to ignore a user in the deaddrop temporarily ("Not now" button).
* Mark messages as presented to the user.
* Typically, UIs call this function on scrolling through the chatlist,
* when the messages are presented at least for a little moment.
* The concrete action depends on the type of the chat and on the users settings
* (dc_msgs_presented() may be a better name therefore, but well :)
*
* The contact is expected to belong to the deaddrop;
* only one #DC_EVENT_MSGS_NOTICED with chat_id=DC_CHAT_ID_DEADDROP may be emitted.
* - For normal chats, the IMAP state is updated, MDN is sent
* (if dc_set_config()-options `mdns_enabled` is set)
* and the internal state is changed to DC_STATE_IN_SEEN to reflect these actions.
*
* See also dc_marknoticed_chat() and dc_markseen_msgs()
* - For contact requests, no IMAP or MDNs is done
* and the internal state is not changed therefore.
* See also dc_marknoticed_chat().
*
* @deprecated Deprecated 2021-02-07, use dc_decide_on_contact_request() if the user just hit "Not now" on a button in the deaddrop,
* dc_marknoticed_chat() if the user has entered a chat
* and dc_markseen_msgs() if the user actually _saw_ a message.
* @memberof dc_context_t
* @param context The context object.
* @param contact_id The contact ID of which all messages should be marked as noticed.
*/
void dc_marknoticed_contact (dc_context_t* context, uint32_t contact_id);
/**
* Mark a message as _seen_, updates the IMAP state and
* sends MDNs. If the message is not in a real chat (e.g. a contact request), the
* message is only marked as NOTICED and no IMAP/MDNs is done. See also
* dc_marknoticed_chat().
*
* Moreover, if messages belong to a chat with ephemeral messages enabled,
* the ephemeral timer is started for these messages.
* Moreover, timer is started for incoming ephemeral messages.
* This also happens for contact requests chats.
*
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
*
@@ -1620,53 +1660,6 @@ void dc_markseen_msgs (dc_context_t* context, const uint3
dc_msg_t* dc_get_msg (dc_context_t* context, uint32_t msg_id);
#define DC_DECISION_START_CHAT 0
#define DC_DECISION_BLOCK 1
#define DC_DECISION_NOT_NOW 2
/**
* Call this when the user decided about a deaddrop message ("Do you want to chat with NAME?").
*
* Possible decisions are:
* - DC_DECISION_START_CHAT (0)
* - This will create a new chat and return the chat id.
* - DC_DECISION_BLOCK (1)
* - This will block the sender.
* - When a new message from the sender arrives,
* that will not result in a new contact request.
* - The blocked sender will be returned by dc_get_blocked_contacts()
* typically, the UI offers an option to unblock senders from there.
* - DC_DECISION_NOT_NOW (2)
* - This will mark all messages from this sender as noticed.
* - That the contact request is removed from the chat list.
* - When a new message from the sender arrives,
* a new contact request with the new message will pop up in the chatlist.
* - The contact request stays available in the explicit deaddrop.
* - If the contact request is already noticed, nothing happens.
*
* If the message belongs to a mailing list,
* the function makes sure that all messages
* from the mailing list are blocked or marked as noticed.
*
* The user should be asked whether they want to chat with the _contact_ belonging to the message;
* the group names may be really weird when taken from the subject of implicit (= ad-hoc)
* groups and this may look confusing. Moreover, this function also scales up the origin of the contact.
*
* If the chat belongs to a mailing list, you can also ask
* "Would you like to read MAILING LIST NAME?"
* (use dc_msg_get_real_chat_id() to get the chat-id for the contact request
* and then dc_chat_is_mailing_list(), dc_chat_get_name() and so on)
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id ID of Message to decide on.
* @param decision One of the DC_DECISION_* values.
* @return The chat id of the created chat, if any.
*/
uint32_t dc_decide_on_contact_request (dc_context_t* context, uint32_t msg_id, int decision);
// handle contacts
/**
@@ -1860,7 +1853,8 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
/**
* Import/export things.
* During backup import/export IO must not be started, if needed stop IO using dc_stop_io() first.
* During backup import/export IO must not be started,
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
* What to do is defined by the _what_ parameter which may be one of the following:
*
* - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`.
@@ -2058,27 +2052,80 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
#define DC_QR_ERROR 400 // text1=error string
#define DC_QR_WITHDRAW_VERIFYCONTACT 500
#define DC_QR_WITHDRAW_VERIFYGROUP 502 // text1=groupname
#define DC_QR_REVIVE_VERIFYCONTACT 510
#define DC_QR_REVIVE_VERIFYGROUP 512 // text1=groupname
/**
* Check a scanned QR code.
* The function should be called after a QR code is scanned.
* The function takes the raw text scanned and checks what can be done with it.
*
* The UI is supposed to show the result to the user.
* In case there are further actions possible,
* the UI has to ask the user before doing further steps.
*
* The QR code state is returned in dc_lot_t::state as:
*
* - DC_QR_ASK_VERIFYCONTACT with dc_lot_t::id=Contact ID
* - DC_QR_ASK_VERIFYGROUP withdc_lot_t::text1=Group name
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID
* - DC_QR_ASK_VERIFYCONTACT with dc_lot_t::id=Contact ID:
* ask whether to verify the contact;
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_ASK_VERIFYGROUP withdc_lot_t::text1=Group name:
* ask whether to join the group;
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID:
* contact fingerprint verified,
* ask the user if they want to start chatting;
* if so, call dc_create_chat_by_contact_id().
*
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID:
* scanned fingerprint does not match last seen fingerprint.
*
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
* - DC_QR_ACCOUNT allows creation of an account, dc_lot_t::text1=domain
* - DC_QR_WEBRTC_INSTANCE - a shared webrtc-instance
* that will be set if dc_set_config_from_qr() is called with the qr-code,
* dc_lot_t::text1=domain could be used to ask the user
* - DC_QR_ADDR with dc_lot_t::id=Contact ID
* - DC_QR_TEXT with dc_lot_t::text1=Text
* - DC_QR_URL with dc_lot_t::text1=URL
* - DC_QR_ERROR with dc_lot_t::text1=Error string
* the scanned QR code contains a fingerprint but no email address;
* suggest the user to establish an encrypted connection first.
*
* - DC_QR_ACCOUNT dc_lot_t::text1=domain:
* ask the user if they want to create an account on the given domain,
* if so, call dc_set_config_from_qr() and then dc_configure().
*
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
* ask the user if they want to use the given service for video chats;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
* email-address scanned,
* ask the user if they want to start chatting;
* if so, call dc_create_chat_by_contact_id()
*
* - DC_QR_TEXT with dc_lot_t::text1=Text:
* Text scanned,
* ask the user eg. if they want copy to clipboard.
*
* - DC_QR_URL with dc_lot_t::text1=URL:
* URL scanned,
* ask the user eg. if they want to open a browser or copy to clipboard.
*
* - DC_QR_ERROR with dc_lot_t::text1=Error string:
* show the error to the user.
*
* - DC_QR_WITHDRAW_VERIFYCONTACT:
* ask the user if they want to withdraw the their own qr-code;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_WITHDRAW_VERIFYGROUP with text1=groupname:
* ask the user if they want to withdraw the group-invite code;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_REVIVE_VERIFYCONTACT:
* ask the user if they want to revive their withdrawn qr-code;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_REVIVE_VERIFYGROUP with text1=groupname:
* ask the user if they want to revive the withdrawn group-invite code;
* if so, call dc_set_config_from_qr().
*
* @memberof dc_context_t
* @param context The context object.
@@ -2341,7 +2388,7 @@ void dc_str_unref (char* str);
* The account manager takes an directory
* where all context-databases are placed in.
* To add a context to the account manager,
* use dc_accounts_add_account(), dc_accounts_import_account or dc_accounts_migrate_account().
* use dc_accounts_add_account() or dc_accounts_migrate_account().
* All account information are persisted.
* To remove a context from the account manager,
* use dc_accounts_remove_account().
@@ -2386,21 +2433,6 @@ void dc_accounts_unref (dc_accounts_t* accounts);
uint32_t dc_accounts_add_account (dc_accounts_t* accounts);
/**
* Import a tarfile-backup to the account manager.
* On success, a new account is added to the account-manager,
* with all the data provided by the backup-file.
* Moreover, the newly created account will be the selected one.
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @param tarfile Backup as created by dc_imex().
* @return Account-id, use dc_accounts_get_account() to get the context object.
* On errors, 0 is returned.
*/
uint32_t dc_accounts_import_account (dc_accounts_t* accounts, const char* tarfile);
/**
* Migrate independent accounts into accounts managed by the account manager.
* This will _move_ the database-file and all blob-files to the directory managed
@@ -2470,6 +2502,7 @@ dc_context_t* dc_accounts_get_account (dc_accounts_t* accounts, uint32
* unmanaged account-context as created by dc_context_new().
* Once you do no longer need the context-object, you have to call dc_context_unref() on it,
* which, however, will not close the account but only decrease a reference counter.
* If there is no selected account, NULL is returned.
*/
dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
@@ -2485,6 +2518,23 @@ dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
int dc_accounts_select_account (dc_accounts_t* accounts, uint32_t account_id);
/**
* This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
*
* iOS can:
* - call dc_start_io() (in case IO was not running)
* - call dc_maybe_network()
* - while dc_accounts_all_work_done() returns false:
* - Wait for #DC_EVENT_CONNECTIVITY_CHANGED
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return Whether all accounts finished their background work.
* #DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
*/
int dc_accounts_all_work_done (dc_accounts_t* accounts);
/**
* Start job and IMAP/SMTP tasks for all accounts managed by the account manager.
* If IO is already running, nothing happens.
@@ -2520,6 +2570,21 @@ void dc_accounts_stop_io (dc_accounts_t* accounts);
void dc_accounts_maybe_network (dc_accounts_t* accounts);
/**
* This function can be called when there is a hint that the network is lost.
* This is similar to dc_accounts_maybe_network(), however,
* it does not retry job processing.
*
* dc_accounts_maybe_network_lost() is needed only on systems
* where the core cannot find out the connectivity loss on its own, eg. iOS.
* The function is not needed on Android, MacOS, Windows or Linux.
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
*/
void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
/**
* Create the event emitter that is used to receive events.
*
@@ -2739,15 +2804,9 @@ int dc_array_search_id (const dc_array_t* array, uint32_t
* and for each messages that is scrolled into view, dc_get_msg() is called then.
*
* Why no listflags?
* Without listflags, dc_get_chatlist() adds the deaddrop
* and the archive "link" automatically as needed.
* Without listflags, dc_get_chatlist() adds
* the archive "link" automatically as needed.
* The UI can just render these items differently then.
* Although the deaddrop link is currently always the first entry
* and only present on new messages,
* there is the rough idea that it can be optionally always present
* and sorted into the list by date.
* Rendering the deaddrop in the described way
* would not add extra work in the UI then.
*/
@@ -2888,7 +2947,6 @@ char* dc_chat_get_info_json (dc_context_t* context, size_t chat
*/
#define DC_CHAT_ID_DEADDROP 1 // virtual chat showing all messages belonging to chats flagged with chats.blocked=2
#define DC_CHAT_ID_TRASH 3 // messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
#define DC_CHAT_ID_ARCHIVED_LINK 6 // only an indicator in a chatlist
#define DC_CHAT_ID_ALLDONE_HINT 7 // only an indicator in a chatlist
@@ -2915,7 +2973,6 @@ void dc_chat_unref (dc_chat_t* chat);
* Get chat ID. The chat ID is the ID under which the chat is filed in the database.
*
* Special IDs:
* - DC_CHAT_ID_DEADDROP (1) - Virtual chat containing messages which senders are not confirmed by the user.
* - DC_CHAT_ID_ARCHIVED_LINK (6) - A link at the end of the chatlist, if present the UI should show the button "Archived chats"-
*
* "Normal" chat IDs are larger than these special IDs (larger than DC_CHAT_ID_LAST_SPECIAL).
@@ -2974,7 +3031,7 @@ char* dc_chat_get_name (const dc_chat_t* chat);
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return Path and file if the profile image, if any.
* @return Path and file of the profile image, if any.
* NULL otherwise.
* Must be released using dc_str_unref() after usage.
*/
@@ -3007,6 +3064,25 @@ uint32_t dc_chat_get_color (const dc_chat_t* chat);
int dc_chat_get_visibility (const dc_chat_t* chat);
/**
* Check if a chat is a contact request chat.
*
* UI should display such chats with a [New] badge in the chatlist.
*
* When such chat is opened, user should be presented with a set of
* options instead of the message composition area, for example:
* - Accept chat (dc_accept_chat())
* - Block chat (dc_block_chat())
* - Delete chat (dc_delete_chat())
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat is a contact request chat
* 0=chat is not a contact request chat
*/
int dc_chat_is_contact_request (const dc_chat_t* chat);
/**
* Check if a group chat is still unpromoted.
*
@@ -3059,8 +3135,8 @@ int dc_chat_is_device_talk (const dc_chat_t* chat);
/**
* Check if messages can be sent to a give chat.
* This is not true e.g. for the deaddrop or for the device-talk, cmp. dc_chat_is_device_talk().
* Check if messages can be sent to a given chat.
* This is not true e.g. for contact requests or for the device-talk, cmp. dc_chat_is_device_talk().
*
* Calling dc_send_msg() for these chats will fail
* and the UI may decide to hide input controls therefore.
@@ -3204,9 +3280,6 @@ uint32_t dc_msg_get_from_id (const dc_msg_t* msg);
/**
* Get the ID of chat the message belongs to.
* To get details about the chat, pass the returned ID to dc_get_chat().
* If a message is still in the deaddrop, the ID DC_CHAT_ID_DEADDROP is returned
* although internally another ID is used.
* (to get that internal id, use dc_msg_get_real_chat_id())
*
* @memberof dc_msg_t
* @param msg The message object.
@@ -3215,19 +3288,6 @@ uint32_t dc_msg_get_from_id (const dc_msg_t* msg);
uint32_t dc_msg_get_chat_id (const dc_msg_t* msg);
/**
* Get the ID of chat the message belongs to.
* To get details about the chat, pass the returned ID to dc_get_chat().
* In contrast to dc_msg_get_chat_id(), this function returns the chat-id also
* for messages in the deaddrop.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The ID of the chat the message belongs to, 0 on errors.
*/
uint32_t dc_msg_get_real_chat_id (const dc_msg_t* msg);
/**
* Get the type of the message.
*
@@ -3477,6 +3537,16 @@ int dc_msg_get_duration (const dc_msg_t* msg);
*/
int dc_msg_get_showpadlock (const dc_msg_t* msg);
/**
* Check if incoming message is a bot message, i.e. automatically submitted.
*
* Return value for outgoing messages is unspecified.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=message is submitted automatically, 0=message is not automatically submitted.
*/
int dc_msg_is_bot (const dc_msg_t* msg);
/**
* Get ephemeral timer duration for message.
@@ -3547,17 +3617,31 @@ char* dc_msg_get_summarytext (const dc_msg_t* msg, int approx_c
/**
* Get the name that should be shown over the message (in a group chat) instead of the contact
* display name.
* display name, or NULL.
*
* If this returns non-NULL, put a `~` before the override-sender-name and show the
* override-sender-name and the sender's avatar even in 1:1 chats.
*
* In mailing lists, sender display name and sender address do not always belong together.
* In this case, this function gives you the name that should actually be shown over the message.
*
* Also, sometimes, we need to indicate a different sender in 1:1 chats:
* Suppose that our user writes an email to support@delta.chat, which forwards to
* Bob <bob@delta.chat>, and Bob replies.
*
* Then, Bob's reply is shown in our 1:1 chat with support@delta.chat and the override-sender-name is
* set to `Bob`. The UI should show the sender name as `~Bob` and show the avatar, just
* as in group messages. If the user then taps on the avatar, they can see that this message
* comes from bob@delta.chat.
*
* You should show a `~` before the override-sender-name in chats, so that the user can
* see that this isn't the sender's actual name.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return the name to show over this message or NULL.
* If this returns NULL, call `dc_contact_get_display_name()`.
* The returned string must be released using dc_str_unref().
* Returns an empty string on errors, never returns NULL.
*/
char* dc_msg_get_override_sender_name(const dc_msg_t* msg);
@@ -4872,26 +4956,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_ERROR 400
/**
* An action cannot be performed because there is no network available.
*
* The library will typically try over after a some time
* and when dc_maybe_network() is called.
*
* Network errors should be reported to users in a non-disturbing way,
* however, as network errors may come in a sequence,
* it is not useful to raise each an every error to the user.
*
* Moreover, if the UI detects that the device is offline,
* it is probably more useful to report this to the user
* instead of the string from data2.
*
* @param data1 0
* @param data2 (char*) Error string, always set, never NULL.
*/
#define DC_EVENT_ERROR_NETWORK 401
/**
* An action cannot be performed because the user is not in the group.
* Reported e.g. after a call to
@@ -5076,6 +5140,18 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
/**
* The connectivity to the server changed.
* This means that you should refresh the connectivity view
* and possibly the connectivtiy HTML; see dc_get_connectivity() and
* dc_get_connectivity_html() for details.
*
* @param data1 0
* @param data2 0
*/
#define DC_EVENT_CONNECTIVITY_CHANGED 2100
/**
* @}
*/
@@ -5244,11 +5320,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_VOICEMESSAGE 7
/// "Contact requests"
///
/// Used as the name for the corresponding chat.
#define DC_STR_DEADDROP 8
/// "Image"
///
/// Used in summaries.
@@ -5403,13 +5474,6 @@ void dc_event_unref(dc_event_t* event);
/// - %1$s will be replaced by the failing login name
#define DC_STR_CANNOT_LOGIN 60
/// "Could not connect to %1$s: %2$s"
///
/// Used in error strings.
/// - %1$s will be replaced by the failing server
/// - %2$s by a the error message as returned from the server
#define DC_STR_SERVER_RESPONSE 61
/// "%1$s by %2$s"
///
/// Used to concretize actions,
@@ -5605,6 +5669,11 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
#define DC_STR_EPHEMERAL_WEEKS 96
/// "Forwarded"
///
/// Used in message summary text for notifications and chatlist.
#define DC_STR_FORWARDED 97
/**
* @}
*/

View File

@@ -1,6 +1,6 @@
prefix={prefix}
libdir=${{prefix}}/lib
includedir=${{prefix}}/include
libdir={libdir}
includedir={includedir}
Name: {name}
Description: {description}

File diff suppressed because it is too large Load Diff

View File

@@ -17,15 +17,12 @@ use std::ptr;
/// }
/// ```
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);
assert!(!ret.is_null());
let ret: *mut libc::c_char = if !s.is_null() {
libc::strdup(s)
} else {
ret = libc::calloc(1, 1) as *mut libc::c_char;
assert!(!ret.is_null());
}
libc::calloc(1, 1) as *mut libc::c_char
};
assert!(!ret.is_null());
ret
}
@@ -170,15 +167,20 @@ pub(crate) trait Strdup {
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> Strdup for T {
impl Strdup for str {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::new_lossy(self.as_ref());
let tmp = CString::new_lossy(self);
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 String {
unsafe fn strdup(&self) -> *mut libc::c_char {
let s: &str = self;
s.strdup()
}
}
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());
@@ -186,6 +188,13 @@ impl Strdup for std::path::Path {
}
}
impl Strdup for [u8] {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::new_lossy(self);
dc_strdup(tmp.as_ptr())
}
}
/// Convenience methods to turn optional strings into C strings.
///
/// This is the same as the [Strdup] trait but a different trait name
@@ -241,7 +250,7 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
/// [OsStrExt::to_c_string] requires valid Unicode on Windows, this
/// requires that the pointer contains valid UTF-8 on Windows.
///
/// Because this returns a reference the [Path] silce can not outlive
/// Because this returns a reference the [Path] slice can not outlive
/// the original pointer.
///
/// [Path]: std::path::Path

View File

@@ -9,5 +9,5 @@ license = "MPL-2.0"
proc-macro = true
[dependencies]
syn = "1.0.13"
syn = "1.0.74"
quote = "1.0.2"

View File

@@ -35,7 +35,11 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
impl rusqlite::types::FromSql for #name {
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
let inner = rusqlite::types::FromSql::column_result(col)?;
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
if let Some(value) = num_traits::FromPrimitive::from_i64(inner) {
Ok(value)
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(inner))
}
}
}
};

View File

@@ -17,7 +17,7 @@ use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::lot::LotState;
use deltachat::message::{self, ContactRequestDecision, Message, MessageState, MsgId};
use deltachat::message::{self, Message, MessageState, MsgId};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
@@ -120,11 +120,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
real_spec = spec.to_string();
context
.sql()
.set_raw_config(context, "import_spec", Some(&real_spec))
.set_raw_config("import_spec", Some(&real_spec))
.await
.unwrap();
} else {
let rs = context.sql().get_raw_config(context, "import_spec").await;
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
if rs.is_none() {
error!(context, "Import: No file or folder given.");
return false;
@@ -201,7 +201,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
contact_id,
msgtext.unwrap_or_default(),
if msg.has_html() { "[HAS-HTML]" } else { "" },
if msg.get_from_id() == 1 as libc::c_uint {
if msg.get_from_id() == 1 {
""
} else if msg.get_state() == MessageState::InSeen {
"[SEEN]"
@@ -292,7 +292,7 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
let peerstate = Peerstate::from_addr(context, &addr)
.await
.expect("peerstate error");
if peerstate.is_some() && *contact_id != 1 as libc::c_uint {
if peerstate.is_some() && *contact_id != 1 {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
@@ -351,6 +351,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
configure\n\
connect\n\
disconnect\n\
connectivity\n\
maybenetwork\n\
housekeeping\n\
help imex (Import/Export)\n\
@@ -372,6 +373,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
getlocations [<contact-id>]\n\
send <text>\n\
sendimage <file> [<text>]\n\
sendsticker <file> [<text>]\n\
sendfile <file> [<text>]\n\
sendhtml <file for html-part> [<text for plain-part>]\n\
videochat\n\
@@ -387,10 +389,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
===========================Contact requests==\n\
decidestartchat <msg-id>\n\
decideblock <msg-id>\n\
decidenotnow <msg-id>\n\
accept <chat-id>\n\
decline <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
msginfo <msg-id>\n\
@@ -457,20 +457,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"export-backup" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportBackup, &dir).await?;
imex(&context, ImexMode::ExportBackup, dir.as_ref()).await?;
println!("Exported to {}.", dir.to_string_lossy());
}
"import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
imex(&context, ImexMode::ImportBackup, arg1).await?;
imex(&context, ImexMode::ImportBackup, arg1.as_ref()).await?;
}
"export-keys" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportSelfKeys, &dir).await?;
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref()).await?;
println!("Exported to {}.", dir.to_string_lossy());
}
"import-keys" => {
imex(&context, ImexMode::ImportSelfKeys, arg1).await?;
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref()).await?;
}
"export-setup" => {
let setup_code = create_setup_code(&context);
@@ -510,6 +510,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"info" => {
println!("{:#?}", context.get_info().await);
}
"connectivity" => {
let file = dirs::home_dir()
.unwrap_or_default()
.join("connectivity.html");
let html = context.get_connectivity_html().await;
fs::write(&file, html)?;
println!("Report written to: {:#?}", file);
}
"maybenetwork" => {
context.maybe_network().await;
}
@@ -526,9 +534,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
None,
)
.await?;
let time_needed = std::time::SystemTime::now()
.duration_since(time_start)
.unwrap_or_default();
let time_needed = time_start.elapsed().unwrap_or_default();
let cnt = chatlist.len();
if cnt > 0 {
@@ -539,11 +545,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
println!(
"{}#{}: {} [{} fresh] {}{}{}",
"{}#{}: {} [{} fresh] {}{}{}{}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
chat.get_id().get_fresh_msg_cnt(&context).await,
chat.get_id().get_fresh_msg_cnt(&context).await?,
if chat.is_muted() { "🔇" } else { "" },
match chat.visibility {
ChatVisibility::Normal => "",
@@ -551,8 +557,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ChatVisibility::Pinned => "📌",
},
if chat.is_protected() { "🛡️" } else { "" },
if chat.is_contact_request() {
"🆕"
} else {
""
},
);
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
let lot = chatlist.get_summary(&context, i, Some(&chat)).await?;
let statestr = if chat.visibility == ChatVisibility::Archived {
" [Archived]"
} else {
@@ -605,7 +616,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "Failed to select chat");
let sel_chat = sel_chat.as_ref().unwrap();
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await;
let time_start = std::time::SystemTime::now();
let msglist =
chat::get_chat_msgs(&context, sel_chat.get_id(), DC_GCM_ADDDAYMARKER, None).await?;
let time_needed = time_start.elapsed().unwrap_or_default();
let msglist: Vec<MsgId> = msglist
.into_iter()
.map(|x| match x {
@@ -615,7 +630,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
})
.collect();
let members = chat::get_chat_contacts(&context, sel_chat.id).await;
let members = chat::get_chat_contacts(&context, sel_chat.id).await?;
let subtitle = if sel_chat.is_device_talk() {
"device-talk".to_string()
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
@@ -638,7 +653,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
} else {
""
},
match sel_chat.get_profile_image(&context).await {
match sel_chat.get_profile_image(&context).await? {
Some(icon) => match icon.to_str() {
Some(icon) => format!(" Icon: {}", icon),
_ => " Icon: Err".to_string(),
@@ -658,46 +673,25 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!(
"{} messages.",
sel_chat.get_id().get_msg_cnt(&context).await
sel_chat.get_id().get_msg_cnt(&context).await?
);
let time_noticed_start = std::time::SystemTime::now();
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
println!(
"{:?} to create this list, {:?} to mark all messages as noticed.",
time_needed, time_noticed_needed
);
}
"createchat" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id: libc::c_int = arg1.parse()?;
let chat_id = chat::create_by_contact_id(&context, contact_id as u32).await?;
let contact_id: u32 = arg1.parse()?;
let chat_id = ChatId::create_for_contact(&context, contact_id).await?;
println!("Single#{} created successfully.", chat_id,);
}
"decidestartchat" | "createchatbymsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
match message::decide_on_contact_request(
&context,
msg_id,
ContactRequestDecision::StartChat,
)
.await
{
Some(chat_id) => {
let chat = Chat::load_from_db(&context, chat_id).await?;
println!("{}#{} created successfully.", chat_prefix(&chat), chat_id);
}
None => println!("Cannot crate chat."),
}
}
"decidenotnow" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::NotNow)
.await;
}
"decideblock" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::Block)
.await;
}
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
@@ -716,11 +710,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_0: libc::c_int = arg1.parse()?;
let contact_id_0: u32 = arg1.parse()?;
if chat::add_contact_to_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
contact_id_0 as u32,
contact_id_0,
)
.await
{
@@ -732,11 +726,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"removemember" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_1: libc::c_int = arg1.parse()?;
let contact_id_1: u32 = arg1.parse()?;
chat::remove_contact_from_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
contact_id_1 as u32,
contact_id_1,
)
.await?;
@@ -762,7 +756,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
let contacts =
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await;
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
println!("Memberlist:");
log_contactlist(&context, &contacts).await;
@@ -787,7 +781,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
0,
0,
)
.await;
.await?;
let default_marker = "-".to_string();
for location in &locations {
let marker = location.marker.as_ref().unwrap_or(&default_marker);
@@ -855,12 +849,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
}
"sendimage" | "sendfile" => {
"sendimage" | "sendsticker" | "sendfile" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No file given.");
let mut msg = Message::new(if arg0 == "sendimage" {
Viewtype::Image
} else if arg0 == "sendsticker" {
Viewtype::Sticker
} else {
Viewtype::File
});
@@ -899,10 +895,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
None
};
let msglist = context.search_msgs(chat, arg1).await;
let time_start = std::time::SystemTime::now();
let msglist = context.search_msgs(chat, arg1).await?;
let time_needed = time_start.elapsed().unwrap_or_default();
log_msglist(&context, &msglist).await?;
println!("{} messages.", msglist.len());
println!("{:?} to create this list", time_needed);
}
"draft" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -915,7 +914,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.unwrap()
.get_id()
.set_draft(&context, Some(&mut draft))
.await;
.await?;
println!("Draft saved.");
} else {
sel_chat
@@ -923,7 +922,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.unwrap()
.get_id()
.set_draft(&context, None)
.await;
.await?;
println!("Draft deleted.");
}
}
@@ -946,7 +945,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Viewtype::Gif,
Viewtype::Video,
)
.await;
.await?;
println!("{} images or videos: ", images.len());
for (i, data) in images.iter().enumerate() {
if 0 == i {
@@ -1009,10 +1008,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let chat_id = ChatId::new(arg1.parse()?);
chat_id.delete(&context).await?;
}
"accept" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id.accept(&context).await?;
}
"blockchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id.block(&context).await?;
}
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
let res = message::get_msg_info(&context, id).await;
let res = message::get_msg_info(&context, id).await?;
println!("{}", res);
}
"html" => {
@@ -1021,7 +1030,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let file = dirs::home_dir()
.unwrap_or_default()
.join(format!("msg-{}.html", id.to_u32()));
let html = id.get_html(&context).await.unwrap_or_default();
let html = id.get_html(&context).await?.unwrap_or_default();
fs::write(&file, html)?;
println!("HTML written to: {:#?}", file);
}
@@ -1046,7 +1055,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = vec![MsgId::new(0)];
msg_ids[0] = MsgId::new(arg1.parse()?);
message::markseen_msgs(&context, msg_ids).await;
message::markseen_msgs(&context, msg_ids).await?;
}
"delmsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
@@ -1073,7 +1082,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
if !arg2.is_empty() {
let book = format!("{}\n{}", arg1, arg2);
Contact::add_address_book(&context, book).await?;
Contact::add_address_book(&context, &book).await?;
} else {
Contact::create(&context, "", arg1).await?;
}
@@ -1081,14 +1090,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"contactinfo" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
let contact_id: u32 = arg1.parse()?;
let contact = Contact::get_by_id(&context, contact_id).await?;
let name_n_addr = contact.get_name_n_addr();
let mut res = format!(
"Contact info for: {}:\nIcon: {}\n",
name_n_addr,
match contact.get_profile_image(&context).await {
match contact.get_profile_image(&context).await? {
Some(image) => image.to_str().unwrap().to_string(),
None => "NoIcon".to_string(),
}
@@ -1121,12 +1130,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"block" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
Contact::block(&context, contact_id).await;
Contact::block(&context, contact_id).await?;
}
"unblock" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
Contact::unblock(&context, contact_id).await;
Contact::unblock(&context, contact_id).await?;
}
"listblocked" => {
let contacts = Contact::get_all_blocked(&context).await?;
@@ -1177,7 +1186,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
// println!(
// "Sending event {:?}({}), received value {}.",
// event, event as usize, r as libc::c_int,
// event, event as usize, r,
// );
// }
"fileinfo" => {

View File

@@ -26,8 +26,9 @@ use rustyline::config::OutputStreamType;
use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
use rustyline::hint::{Hinter, HistoryHinter};
use rustyline::validate::Validator;
use rustyline::{
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyPress,
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
};
mod cmdline;
@@ -56,9 +57,6 @@ fn receive_event(event: EventType) {
EventType::Error(msg) => {
error!("{}", msg);
}
EventType::ErrorNetwork(msg) => {
error!("[NETWORK] msg={}", msg);
}
EventType::ErrorSelfNotInGroup(msg) => {
error!("[SELF_NOT_IN_GROUP] {}", msg);
}
@@ -156,7 +154,7 @@ const IMEX_COMMANDS: [&str; 12] = [
"stop",
];
const DB_COMMANDS: [&str; 9] = [
const DB_COMMANDS: [&str; 10] = [
"info",
"set",
"get",
@@ -164,18 +162,16 @@ const DB_COMMANDS: [&str; 9] = [
"configure",
"connect",
"disconnect",
"connectivity",
"maybenetwork",
"housekeeping",
];
const CHAT_COMMANDS: [&str; 34] = [
const CHAT_COMMANDS: [&str; 33] = [
"listchats",
"listarchived",
"chat",
"createchat",
"decidestartchat",
"decideblock",
"decidenotnow",
"creategroup",
"createverified",
"addmember",
@@ -203,6 +199,8 @@ const CHAT_COMMANDS: [&str; 34] = [
"protect",
"unprotect",
"delchat",
"accept",
"blockchat",
];
const MESSAGE_COMMANDS: [&str; 6] = [
"listmsgs",
@@ -237,7 +235,9 @@ const MISC_COMMANDS: [&str; 10] = [
];
impl Hinter for DcHelper {
fn hint(&self, line: &str, pos: usize, ctx: &RustyContext<'_>) -> Option<String> {
type Hint = String;
fn hint(&self, line: &str, pos: usize, ctx: &RustyContext<'_>) -> Option<Self::Hint> {
if !line.is_empty() {
for &cmds in &[
&IMEX_COMMANDS[..],
@@ -259,11 +259,10 @@ impl Hinter for DcHelper {
}
static COLORED_PROMPT: &str = "\x1b[1;32m> \x1b[0m";
static PROMPT: &str = "> ";
impl Highlighter for DcHelper {
fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> {
if prompt == PROMPT {
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(&self, prompt: &'p str, default: bool) -> Cow<'b, str> {
if default {
Borrowed(COLORED_PROMPT)
} else {
Borrowed(prompt)
@@ -284,6 +283,7 @@ impl Highlighter for DcHelper {
}
impl Helper for DcHelper {}
impl Validator for DcHelper {}
async fn start(args: Vec<String>) -> Result<(), Error> {
if args.len() < 2 {
@@ -317,8 +317,8 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
};
let mut rl = Editor::with_config(config);
rl.set_helper(Some(h));
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
rl.bind_sequence(KeyEvent::alt('N'), Cmd::HistorySearchForward);
rl.bind_sequence(KeyEvent::alt('P'), Cmd::HistorySearchBackward);
if rl.load_history(".dc-history.txt").is_err() {
println!("No previous history.");
}
@@ -390,7 +390,7 @@ async fn handle_cmd(
ctx.configure().await?;
}
"oauth2" => {
if let Some(addr) = ctx.get_config(config::Config::Addr).await {
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
let oauth2_url =
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
if oauth2_url.is_none() {

View File

@@ -1,6 +1,6 @@
use tempfile::tempdir;
use deltachat::chat;
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::contact::*;
@@ -19,7 +19,7 @@ fn cb(event: EventType) {
EventType::Warning(msg) => {
log::warn!("{}", msg);
}
EventType::Error(msg) | EventType::ErrorNetwork(msg) => {
EventType::Error(msg) => {
log::error!("{}", msg);
}
event => {
@@ -70,7 +70,7 @@ async fn main() {
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
.await
.unwrap();
let chat_id = chat::create_by_contact_id(&ctx, contact_id).await.unwrap();
let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap();
for i in 0..1 {
log::info!("sending message {}", i);
@@ -86,7 +86,7 @@ async fn main() {
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
for i in 0..chats.len() {
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap())
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
.await
.unwrap();
log::info!("[{}] msg: {:?}", i, msg);

View File

@@ -1,3 +1,10 @@
1.51.0
------
- adapt python bindings and APIs to core51 release
(see CHANGELOG of https://github.com/deltachat/deltachat-core-rust/blob/1.51.0/CHANGELOG.md#1510
for more details on all core changes)
1.44.0
------

View File

@@ -58,12 +58,13 @@ end-to-end tests that require accounts on real e-mail servers.
running "live" tests with temporary accounts
---------------------------------------------
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL``::
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLS created and managed by [mailadm](https://mailadm.readthedocs.io/en/latest/).
export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=1h_4w4r8h7y9nmcdsy
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this:
With this, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
These accounts exists for one 1hour and then are removed completely.
export DCC_NEW_TMP_EMAIL=<URL you got from us>
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server. These accounts exists only for one hour and then are removed completely.
One hour is enough to invoke pytest and run all offline and online tests:
pytest
@@ -142,7 +143,7 @@ This docker image can be used to run tests and build Python wheels for all inter
$ docker run -e DCC_NEW_TMP_EMAIL \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh
deltachat/coredeps scripts/run_all.sh
Optionally build your own docker image
@@ -151,9 +152,9 @@ Optionally build your own docker image
If you want to build your own custom docker image you can do this::
$ cd deltachat-core # cd to deltachat-core checkout directory
$ docker build -t deltachat/coredeps ci_scripts/docker_coredeps
$ docker build -t deltachat/coredeps scripts/docker_coredeps
This will use the ``ci_scripts/docker_coredeps/Dockerfile`` to build
This will use the ``scripts/docker_coredeps/Dockerfile`` to build
up docker image called ``deltachat/coredeps``. You can afterwards
find it with::

View File

@@ -18,7 +18,7 @@ def main():
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
long_description=long_description,
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient'],
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient', 'requests'],
packages=setuptools.find_packages('src'),
package_dir={'': 'src'},
cffi_modules=['src/deltachat/_build.py:ffibuilder'],

View File

@@ -9,8 +9,6 @@ import subprocess
import tempfile
import textwrap
import types
from os.path import abspath
from os.path import dirname as dn
import cffi
@@ -50,6 +48,7 @@ def system_build_flags():
flags.objs = []
flags.incs = []
flags.extra_link_args = []
return flags
def extract_functions(flags):
@@ -151,6 +150,7 @@ def extract_defines(flags):
| DC_PROVIDER
| DC_KEY_GEN
| DC_IMEX
| DC_CONNECTIVITY
) # End of prefix matching
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
) # Close the capturing group, this contains
@@ -168,11 +168,8 @@ def extract_defines(flags):
def ffibuilder():
projdir = os.environ.get('DCC_RS_DEV')
if not projdir:
p = dn(dn(dn(dn(abspath(__file__)))))
projdir = os.environ["DCC_RS_DEV"] = p
target = os.environ.get('DCC_RS_TARGET', 'release')
if projdir:
target = os.environ.get('DCC_RS_TARGET', 'release')
flags = local_build_flags(projdir, target)
else:
flags = system_build_flags()

View File

@@ -89,6 +89,22 @@ class Account(object):
d[key.lower()] = value
return d
def dump_account_info(self, logfile):
def log(*args, **kwargs):
kwargs["file"] = logfile
print(*args, **kwargs)
log("=============== " + self.get_config("displayname") + " ===============")
cursor = 0
for name, val in self.get_info().items():
entry = "{}={}".format(name.upper(), val)
if cursor + len(entry) > 80:
log("")
cursor = 0
log(entry, end=" ")
cursor += len(entry) + 1
log("")
def set_stock_translation(self, id, string):
""" set stock translation string.
@@ -314,9 +330,6 @@ class Account(object):
""" Create a 1:1 chat with Account, Contact or e-mail address. """
return self.create_contact(obj).create_chat()
def _create_chat_by_message_id(self, msg_id):
return Chat(self, lib.dc_create_chat_by_msg_id(self._dc_context, msg_id))
def create_group_chat(self, name, contacts=None, verified=False):
""" create a new group chat object.
@@ -351,9 +364,6 @@ class Account(object):
chatlist.append(Chat(self, chat_id))
return chatlist
def get_deaddrop_chat(self):
return Chat(self, const.DC_CHAT_ID_DEADDROP)
def get_device_chat(self):
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
@@ -558,6 +568,15 @@ class Account(object):
""" Stop ongoing securejoin, configuration or other core jobs. """
lib.dc_stop_ongoing_process(self._dc_context)
def get_connectivity(self):
return lib.dc_get_connectivity(self._dc_context)
def get_connectivity_html(self):
return from_dc_charpointer(lib.dc_get_connectivity_html(self._dc_context))
def all_work_done(self):
return lib.dc_all_work_done(self._dc_context)
def start_io(self):
""" start this account's IO scheduling (Rust-core async scheduler)

View File

@@ -50,6 +50,14 @@ class Chat(object):
"""
lib.dc_delete_chat(self.account._dc_context, self.id)
def block(self):
"""Block this chat."""
lib.dc_block_chat(self.account._dc_context, self.id)
def accept(self):
"""Accept this contact request chat."""
lib.dc_accept_chat(self.account._dc_context, self.id)
# ------ chat status/metadata API ------------------------------
def is_group(self):
@@ -59,13 +67,6 @@ class Chat(object):
"""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
def is_deaddrop(self):
""" return true if this chat is a deaddrop chat.
:returns: True if chat is the deaddrop chat, False otherwise.
"""
return self.id == const.DC_CHAT_ID_DEADDROP
def is_muted(self):
""" return true if this chat is muted.
@@ -73,6 +74,13 @@ class Chat(object):
"""
return lib.dc_chat_is_muted(self._dc_chat)
def is_contact_request(self):
""" return True if this chat is a contact request chat.
:returns: True if chat is a contact request chat, False otherwise.
"""
return lib.dc_chat_is_contact_request(self._dc_chat)
def is_promoted(self):
""" return True if this chat is promoted, i.e.
the member contacts are aware of their membership,
@@ -84,7 +92,7 @@ class Chat(object):
def can_send(self):
"""Check if messages can be sent to a give chat.
This is not true eg. for the deaddrop or for the device-talk
This is not true eg. for the contact requests or for the device-talk
:returns: True if the chat is writable, False otherwise
"""
@@ -254,17 +262,19 @@ class Chat(object):
return Message.from_db(self.account, sent_id)
def prepare_message(self, msg):
""" create a new prepared message.
""" prepare a message for sending.
:param msg: the message to be prepared.
:returns: :class:`deltachat.message.Message` instance.
:returns: a :class:`deltachat.message.Message` instance.
This is the same object that was passed in, which
has been modified with the new state of the core.
"""
msg_id = lib.dc_prepare_msg(self.account._dc_context, self.id, msg._dc_msg)
if msg_id == 0:
raise ValueError("message could not be prepared")
# invalidate passed in message which is not safe to use anymore
msg._dc_msg = msg.id = None
return Message.from_db(self.account, msg_id)
# modify message in place to avoid bad state for the caller
msg._dc_msg = Message.from_db(self.account, msg_id)._dc_msg
return msg
def prepare_message_file(self, path, mime_type=None, view_type="file"):
""" prepare a message for sending and return the resulting Message instance.

View File

@@ -47,8 +47,9 @@ def dc_account_extra_configure(account):
except Exception as e:
# Uncaught exceptions here would lead to a timeout without any note written to the log
account.log("=============================== CAN'T RESET ACCOUNT: ===============================")
account.log("===================", e, "===================")
# start with DC_EVENT_WARNING so that the line is printed in yellow and won't be overlooked when reading
account.log("DC_EVENT_WARNING =================== DIRECT_IMAP CAN'T RESET ACCOUNT: ===================")
account.log("DC_EVENT_WARNING =================== " + str(e) + " ===================")
@deltachat.global_hookimpl
@@ -172,21 +173,6 @@ class DirectImap:
def get_unread_cnt(self):
return len(self.get_unread_messages())
def dump_account_info(self, logfile):
def log(*args, **kwargs):
kwargs["file"] = logfile
print(*args, **kwargs)
cursor = 0
for name, val in self.account.get_info().items():
entry = "{}={}".format(name.upper(), val)
if cursor + len(entry) > 80:
log("")
cursor = 0
log(entry, end=" ")
cursor += len(entry) + 1
log("")
def dump_imap_structures(self, dir, logfile):
assert not self._idling
stream = io.StringIO()
@@ -265,7 +251,16 @@ class DirectImap:
return res
def append(self, folder, msg):
"""Upload a message to *folder*.
Trailing whitespace or a linebreak at the beginning will be removed automatically.
"""
if msg.startswith("\n"):
msg = msg[1:]
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
self.conn.append(folder, msg)
def get_uid_by_message_id(self, message_id):
msgs = self.conn.search(['HEADER', 'MESSAGE-ID', message_id])
if len(msgs) == 0:
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
return msgs[0]

View File

@@ -111,6 +111,33 @@ class FFIEventTracker:
if m is not None:
return m.groups()
def wait_for_connectivity(self, connectivity):
"""Wait for the specified connectivity.
This only works reliably if the connectivity doesn't change
again too quickly, otherwise we might miss it."""
while 1:
if self.account.get_connectivity() == connectivity:
return
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def wait_for_connectivity_change(self, previous, expected_next):
"""Wait until the connectivity changes to `expected_next`.
Fails the test if it changes to something else."""
while 1:
current = self.account.get_connectivity()
if current == expected_next:
return
elif current != previous:
raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current))
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def wait_for_all_work_done(self):
while 1:
if self.account.all_work_done():
return
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile("(?:{}).*".format(event_name_regex))

View File

@@ -43,7 +43,7 @@ class PerAccount:
@account_hookspec
def ac_incoming_message(self, message):
""" Called on any incoming message (to deaddrop or chat). """
""" Called on any incoming message (both existing chats and contact requests). """
@account_hookspec
def ac_outgoing_message(self, message):

View File

@@ -21,8 +21,8 @@ class Message(object):
assert isinstance(dc_msg, ffi.CData)
assert dc_msg != ffi.NULL
self._dc_msg = dc_msg
self.id = lib.dc_msg_get_id(dc_msg)
assert self.id is not None and self.id >= 0, repr(self.id)
msg_id = self.id
assert msg_id is not None and msg_id >= 0, repr(msg_id)
def __eq__(self, other):
return self.account == other.account and self.id == other.id
@@ -46,9 +46,13 @@ class Message(object):
def new_empty(cls, account, view_type):
""" create a non-persistent message.
:param: view_type is "text", "audio", "video", "file"
:param: view_type is the message type code or one of the strings:
"text", "audio", "video", "file", "sticker"
"""
view_type_code = get_viewtype_code_from_name(view_type)
if isinstance(view_type, int):
view_type_code = view_type
else:
view_type_code = get_viewtype_code_from_name(view_type)
return Message(account, ffi.gc(
lib.dc_msg_new(account._dc_context, view_type_code),
lib.dc_msg_unref
@@ -57,16 +61,18 @@ class Message(object):
def create_chat(self):
""" create or get an existing chat (group) object for this message.
If the message is a deaddrop contact request
If the message is a contact request
the sender will become an accepted contact.
:returns: a :class:`deltachat.chat.Chat` object.
"""
from .chat import Chat
chat_id = lib.dc_create_chat_by_msg_id(self.account._dc_context, self.id)
ctx = self.account._dc_context
self._dc_msg = ffi.gc(lib.dc_get_msg(ctx, self.id), lib.dc_msg_unref)
return Chat(self.account, chat_id)
self.chat.accept()
return self.chat
@props.with_doc
def id(self):
"""id of this message. """
return lib.dc_msg_get_id(self._dc_msg)
@props.with_doc
def text(self):
@@ -77,6 +83,23 @@ class Message(object):
"""set text of this message. """
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
@props.with_doc
def html(self):
"""html text of this messages (might be empty if not an html message). """
return from_dc_charpointer(
lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
def has_html(self):
"""return True if this message has an html part, False otherwise."""
return lib.dc_msg_has_html(self._dc_msg)
def set_html(self, html_text):
"""set the html part of this message.
It is possible to have text and html part at the same time.
"""
lib.dc_msg_set_html(self._dc_msg, as_dc_charpointer(html_text))
@props.with_doc
def filename(self):
"""filename if there was an attachment, otherwise empty string. """
@@ -115,6 +138,10 @@ class Message(object):
""" return True if this message was encrypted. """
return bool(lib.dc_msg_get_showpadlock(self._dc_msg))
def is_bot(self):
""" return True if this message is submitted automatically. """
return bool(lib.dc_msg_is_bot(self._dc_msg))
def is_forwarded(self):
""" return True if this message was forwarded. """
return bool(lib.dc_msg_is_forwarded(self._dc_msg))
@@ -227,6 +254,20 @@ class Message(object):
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
return Chat(self.account, chat_id)
@props.with_doc
def override_sender_name(self):
"""the name that should be shown over the message instead of the contact display name.
Usually used to impersonate someone else.
"""
return from_dc_charpointer(
lib.dc_msg_get_override_sender_name(self._dc_msg))
def set_override_sender_name(self, name):
"""set different sender name for a message. """
lib.dc_msg_set_override_sender_name(
self._dc_msg, as_dc_charpointer(name))
def get_sender_chat(self):
"""return the 1:1 chat with the sender of this message.
@@ -339,6 +380,10 @@ class Message(object):
""" return True if it's a gif message. """
return self._view_type == const.DC_MSG_GIF
def is_sticker(self):
""" return True if it's a sticker message. """
return self._view_type == const.DC_MSG_STICKER
def is_audio(self):
""" return True if it's an audio message. """
return self._view_type == const.DC_MSG_AUDIO
@@ -359,21 +404,22 @@ class Message(object):
# some code for handling DC_MSG_* view types
_view_type_mapping = {
const.DC_MSG_TEXT: 'text',
const.DC_MSG_IMAGE: 'image',
const.DC_MSG_GIF: 'gif',
const.DC_MSG_AUDIO: 'audio',
const.DC_MSG_VIDEO: 'video',
const.DC_MSG_FILE: 'file'
'text': const.DC_MSG_TEXT,
'image': const.DC_MSG_IMAGE,
'gif': const.DC_MSG_GIF,
'audio': const.DC_MSG_AUDIO,
'video': const.DC_MSG_VIDEO,
'file': const.DC_MSG_FILE,
'sticker': const.DC_MSG_STICKER,
}
def get_viewtype_code_from_name(view_type_name):
for code, value in _view_type_mapping.items():
if value == view_type_name:
return code
code = _view_type_mapping.get(view_type_name)
if code is not None:
return code
raise ValueError("message typecode not found for {!r}, "
"available {!r}".format(view_type_name, list(_view_type_mapping.values())))
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())))
#

View File

@@ -414,13 +414,13 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def dump_imap_summary(self, logfile):
for ac in self._accounts:
ac.dump_account_info(logfile=logfile)
imap = getattr(ac, "direct_imap", None)
if imap is not None:
try:
imap.idle_done()
except Exception:
pass
imap.dump_account_info(logfile=logfile)
imap.dump_imap_structures(tmpdir, logfile=logfile)
def get_accepted_chat(self, ac1, ac2):

View File

@@ -1,15 +1,17 @@
import os
import sys
import platform
import subprocess
import sys
if __name__ == "__main__":
assert len(sys.argv) == 2
workspacedir = sys.argv[1]
arch = platform.machine()
for relpath in os.listdir(workspacedir):
if relpath.startswith("deltachat"):
p = os.path.join(workspacedir, relpath)
subprocess.check_call(
["auditwheel", "repair", p, "-w", workspacedir,
"--plat", "manylinux2014_x86_64"])
"--plat", "manylinux2014_" + arch])

View File

@@ -816,6 +816,48 @@ class TestOnlineAccount:
assert open(msg.filename).read() == content
assert msg.filename.endswith(basename)
def test_html_message(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = acfactory.get_accepted_chat(ac1, ac2)
html_text = "<p>hello HTML world</p>"
lp.sec("ac1: prepare and send text message to ac2")
msg1 = chat.send_text("message0")
assert not msg1.has_html()
assert msg1.html == ""
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message0"
assert not msg2.has_html()
assert msg2.html == ""
lp.sec("ac1: prepare and send HTML+text message to ac2")
msg1 = Message.new_empty(ac1, "text")
msg1.set_text("message1")
msg1.set_html(html_text)
msg1 = chat.send_msg(msg1)
assert msg1.has_html()
assert html_text in msg1.html
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message1"
assert msg2.has_html()
assert html_text in msg2.html
lp.sec("ac1: prepare and send HTML-only message to ac2")
msg1 = Message.new_empty(ac1, "text")
msg1.set_html(html_text)
msg1 = chat.send_msg(msg1)
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert "<p>" not in msg2.text
assert "hello HTML world" in msg2.text
assert msg2.has_html()
assert html_text in msg2.html
def test_mvbox_sentbox_threads(self, acfactory, lp):
lp.sec("ac1: start with mvbox thread")
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True, sentbox=True)
@@ -867,12 +909,11 @@ class TestOnlineAccount:
msg_in = ac2.get_message_by_id(msg_out.id)
assert msg_in.text == "message2"
lp.sec("ac2: check that the message arrive in deaddrop")
lp.sec("ac2: check that the message arrived in a chat")
chat2 = msg_in.chat
assert msg_in in chat2.get_messages()
assert not msg_in.is_forwarded()
assert chat2.is_deaddrop()
assert chat2 == ac2.get_deaddrop_chat()
assert chat2.is_contact_request()
lp.sec("ac2: create new chat and forward message to it")
chat3 = ac2.create_group_chat("newgroup")
@@ -937,16 +978,16 @@ class TestOnlineAccount:
assert not msg2.is_forwarded()
assert msg2.get_sender_contact().display_name == ac1.get_config("displayname")
lp.sec("check the message arrived in contact-requests/deaddrop")
lp.sec("check the message arrived in contact request chat")
chat2 = msg2.chat
assert msg2 in chat2.get_messages()
assert chat2.is_deaddrop()
assert chat2.count_fresh_messages() == 0
assert chat2.is_contact_request()
assert chat2.count_fresh_messages() == 1
assert msg2.time_received >= msg1.time_sent
lp.sec("create new chat with contact and verify it's proper")
chat2b = msg2.create_chat()
assert not chat2b.is_deaddrop()
assert not chat2b.is_contact_request()
assert chat2b.count_fresh_messages() == 1
lp.sec("mark chat as noticed")
@@ -991,16 +1032,75 @@ class TestOnlineAccount:
except queue.Empty:
pass # mark_seen_messages() has generated events before it returns
def test_moved_markseen(self, acfactory, lp):
"""Test that message already moved to DeltaChat folder is marked as seen."""
ac1 = acfactory.get_online_configuring_account(mvbox=True, config={"inbox_watch": "0"})
ac2 = acfactory.get_online_configuring_account()
acfactory.wait_configure_and_start_io([ac1, ac2])
ac1.set_config("bcc_self", "1")
ac1.direct_imap.idle_start()
ac1.create_chat(ac2).send_text("Hello!")
ac1.direct_imap.idle_check(terminate=True)
ac1.stop_io()
# Emulate moving of the message to DeltaChat folder by Sieve rule.
# mailcow server contains this rule by default.
ac1.direct_imap.conn.move(["*"], "DeltaChat")
ac1.direct_imap.select_folder("DeltaChat")
ac1.direct_imap.idle_start()
ac1.start_io()
ac1.direct_imap.idle_wait_for_seen()
ac1.direct_imap.idle_done()
fetch = list(ac1.direct_imap.conn.fetch("*", b'FLAGS').values())
flags = fetch[-1][b'FLAGS']
is_seen = b'\\Seen' in flags
assert is_seen
def test_message_override_sender_name(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = acfactory.get_accepted_chat(ac1, ac2)
overridden_name = "someone else"
ac1.set_config("displayname", "ac1")
lp.sec("sending text message with overridden name from ac1 to ac2")
msg1 = Message.new_empty(ac1, "text")
msg1.set_override_sender_name(overridden_name)
msg1.set_text("message1")
msg1 = chat.send_msg(msg1)
assert msg1.override_sender_name == overridden_name
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message1"
assert msg2.get_sender_contact().name == ac1.get_config("displayname")
assert msg2.override_sender_name == overridden_name
lp.sec("sending normal text message from ac1 to ac2")
msg1 = Message.new_empty(ac1, "text")
msg1.set_text("message2")
msg1 = chat.send_msg(msg1)
assert not msg1.override_sender_name
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message2"
assert msg2.get_sender_contact().name == ac1.get_config("displayname")
assert not msg2.override_sender_name
@pytest.mark.parametrize("mvbox_move", [True, False])
def test_markseen_message_and_mdn(self, acfactory, mvbox_move):
# Please only change this test if you are very sure that it will still catch the issues it catches now.
# We had so many problems with markseen, if in doubt, rather create another test, it can't harm.
ac1 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
ac2 = acfactory.get_online_configuring_account(move=mvbox_move, mvbox=mvbox_move)
acfactory.wait_configure_and_start_io()
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2._evtracker.wait_next_incoming_message()
acfactory.wait_configure_and_start_io()
# Do not send BCC to self, we only want to test MDN on ac1.
ac1.set_config("bcc_self", "0")
folder = "mvbox" if mvbox_move else "inbox"
ac1.direct_imap.select_config_folder(folder)
@@ -1008,6 +1108,9 @@ class TestOnlineAccount:
ac1.direct_imap.idle_start()
ac2.direct_imap.idle_start()
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
msg = ac2._evtracker.wait_next_incoming_message()
ac2.mark_seen_messages([msg])
ac1.direct_imap.idle_wait_for_seen() # Check that the mdn is marked as seen
@@ -1215,9 +1318,11 @@ class TestOnlineAccount:
assert not device_chat.can_send()
assert device_chat.get_draft() is None
def test_dont_show_emails_in_draft_folder(self, acfactory):
def test_dont_show_emails_in_draft_folder(self, acfactory, lp):
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email."""
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
ac1 = acfactory.get_online_configuring_account()
ac1.set_config("show_emails", "2")
ac1.create_contact("alice@example.com").create_chat()
@@ -1238,7 +1343,7 @@ class TestOnlineAccount:
Message-ID: <aepiors@example.org>
Content-Type: text/plain; charset=utf-8
message in Drafts
message in Drafts that is moved to Sent later
""".format(ac1.get_config("configured_addr")))
ac1.direct_imap.append("Sent", """
From: ac1 <{}>
@@ -1251,6 +1356,7 @@ class TestOnlineAccount:
""".format(ac1.get_config("configured_addr")))
ac1.set_config("scan_all_folders_debounce_secs", "0")
lp.sec("All prepared, now let DC find the message")
ac1.start_io()
msg = ac1._evtracker.wait_next_messages_changed()
@@ -1261,6 +1367,18 @@ class TestOnlineAccount:
assert msg.text == "subj message in Sent"
assert len(msg.chat.get_messages()) == 1
ac1.stop_io()
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
ac1.direct_imap.select_folder("Drafts")
uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org")
ac1.direct_imap.conn.move(uid, "Sent")
ac1.start_io()
msg2 = ac1._evtracker.wait_next_messages_changed()
assert msg2.text == "subj message in Drafts that is moved to Sent later"
assert len(msg.chat.get_messages()) == 2
def test_prefer_encrypt(self, acfactory, lp):
"""Test quorum rule for encryption preference in 1:1 and group chat."""
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
@@ -1311,6 +1429,33 @@ class TestOnlineAccount:
# Majority prefers encryption now
assert msg5.is_encrypted()
def test_bot(self, acfactory, lp):
"""Test that bot messages can be identified as such"""
ac1, ac2 = acfactory.get_two_online_accounts()
ac1.set_config("bot", "0")
ac2.set_config("bot", "1")
lp.sec("ac1: create chat with ac2")
chat = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("sending a message from ac1 to ac2")
text1 = "hello"
chat.send_text(text1)
lp.sec("wait for ac2 to receive a message")
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == text1
assert not msg_in.is_bot()
lp.sec("sending a message from ac2 to ac1")
text2 = "reply"
msg_in.chat.send_text(text2)
lp.sec("wait for ac1 to receive a message")
msg_in = ac1._evtracker.wait_next_incoming_message()
assert msg_in.text == text2
assert msg_in.is_bot()
def test_quote_encrypted(self, acfactory, lp):
"""Test that replies to encrypted messages with quotes are encrypted."""
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1498,6 +1643,12 @@ class TestOnlineAccount:
original_image_path = data.get_path("d.png")
chat1.send_image(original_image_path)
# Add another 100KB file that ensures that the progress is smooth enough
path = tmpdir.join("attachment.txt")
with open(path, "w") as file:
file.truncate(100000)
chat1.send_file(path.strpath)
def assert_account_is_proper(ac):
contacts = ac.get_contacts(query="some1")
assert len(contacts) == 1
@@ -1505,7 +1656,7 @@ class TestOnlineAccount:
assert contact2.addr == "some1@example.org"
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
assert len(messages) == 3
assert messages[0].text == "msg1"
assert messages[1].filemime == "image/png"
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
@@ -1633,7 +1784,7 @@ class TestOnlineAccount:
ac1._evtracker.wait_securejoin_inviter_progress(1000)
def test_qr_verified_group_and_chatting(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_protected()
@@ -1664,6 +1815,29 @@ class TestOnlineAccount:
assert msg.text == "world"
assert msg.is_encrypted()
lp.sec("ac1: create QR code and let ac3 scan it, starting the securejoin")
qr = ac1.get_setup_contact_qr()
lp.sec("ac3: start QR-code based setup contact protocol")
ch = ac3.qr_setup_contact(qr)
assert ch.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000)
lp.sec("ac1: add ac3 to verified group")
chat1.add_contact(ac3)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert msg.is_system_message()
assert not msg.error
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")
# Skip system message about added member
ac3._evtracker.wait_next_incoming_message()
msg = ac3._evtracker.wait_next_incoming_message()
assert msg.text == "hi"
assert msg.is_encrypted()
def test_set_get_contact_avatar(self, acfactory, data, lp):
lp.sec("configuring ac1 and ac2")
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1678,7 +1852,7 @@ class TestOnlineAccount:
lp.sec("ac2: wait for receiving message and avatar from ac1")
msg2 = ac2._evtracker.wait_next_messages_changed()
assert msg2.chat.is_deaddrop()
assert msg2.chat.is_contact_request()
received_path = msg2.get_sender_contact().get_profile_image()
assert open(received_path, "rb").read() == open(p, "rb").read()
@@ -1753,6 +1927,8 @@ class TestOnlineAccount:
ev = in_list.get(timeout=10)
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
assert ev.action == "added"
assert ev.message.get_sender_contact().addr == ac1_addr
assert ev.contact.addr == "devnull@testrun.org"
@@ -1867,6 +2043,125 @@ class TestOnlineAccount:
assert msg_back.chat == chat
assert chat.get_profile_image() is None
@pytest.mark.parametrize("inbox_watch", ["0", "1"])
def test_connectivity(self, acfactory, lp, inbox_watch):
ac1, ac2 = acfactory.get_two_online_accounts()
ac1.set_config("inbox_watch", inbox_watch)
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
lp.sec("Test stop_io() and start_io()")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
lp.sec("Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, " +
"all messages are fetched")
ac1.direct_imap.select_config_folder("inbox")
ac1.direct_imap.idle_start()
ac2.create_chat(ac1).send_text("Hi")
ac1.direct_imap.idle_check(terminate=False)
ac1.maybe_network()
ac1._evtracker.wait_for_all_work_done()
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 1
assert msgs[0].text == "Hi"
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
ac2.create_chat(ac1).send_text("Hi 2")
ac1.direct_imap.idle_check(terminate=True)
ac1.maybe_network()
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 2
assert msgs[1].text == "Hi 2"
lp.sec("Test that the connectivity doesn't flicker to WORKING if there are no new messages")
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
lp.sec("Test that the connectivity doesn't flicker to WORKING if the sender of the message is blocked")
ac1.create_contact(ac2).block()
ac1.direct_imap.select_config_folder("inbox")
ac1.direct_imap.idle_start()
ac2.create_chat(ac1).send_text("Hi")
ac1.direct_imap.idle_check(terminate=True)
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong")
ac1.set_config("configured_mail_pw", "abc")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
def test_fetch_deleted_msg(self, acfactory, lp):
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,
hundreds of times, because uid_next was not updated.
See https://github.com/deltachat/deltachat-core-rust/issues/2429.
"""
ac1 = acfactory.get_one_online_account()
ac1.stop_io()
ac1.direct_imap.append("INBOX", """
From: alice <alice@example.org>
Subject: subj
To: bob@example.com
Chat-Version: 1.0
Message-ID: <aepiors@example.org>
Content-Type: text/plain; charset=utf-8
Deleted message
""")
ac1.direct_imap.delete("1:*", expunge=False)
ac1.start_io()
for ev in ac1._evtracker.iter_events():
if ev.name == "DC_EVENT_MSGS_CHANGED":
pytest.fail("A deleted message was shown to the user")
if ev.name == "DC_EVENT_INFO" and "1 mails read from" in ev.data2:
break
# The message was downloaded once, now check that it's not downloaded again
for ev in ac1._evtracker.iter_events():
if ev.name == "DC_EVENT_INFO" and "1 mails read from" in ev.data2:
pytest.fail("The same email was read twice")
if ev.name == "DC_EVENT_MSGS_CHANGED":
pytest.fail("A deleted message was shown to the user")
if ev.name == "DC_EVENT_INFO" and "INBOX: Idle entering wait-on-remote state" in ev.data2:
break # DC is done with reading messages
def test_send_receive_locations(self, acfactory, lp):
now = datetime.utcnow()
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -2187,9 +2482,25 @@ class TestOnlineAccount:
chat12 = acfactory.get_accepted_chat(ac1, ac2)
ac1.set_config("selfstatus", "New status")
chat12.send_text("hi")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hi"
assert msg.get_sender_contact().status == "New status"
msg_received = ac2._evtracker.wait_next_incoming_message()
assert msg_received.text == "hi"
assert msg_received.get_sender_contact().status == "New status"
# Send a reply from ac2 to ac1 so ac1 can send a read receipt.
reply_msg = msg_received.chat.send_text("reply")
reply_msg_received = ac1._evtracker.wait_next_incoming_message()
assert reply_msg_received.text == "reply"
# Send read receipt from ac1 to ac2.
# It does not contain the signature.
ac1.mark_seen_messages([reply_msg_received])
ev = ac2._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 == reply_msg.chat.id
assert ev.data2 == reply_msg.id
assert reply_msg.is_out_mdn_received()
# Test that the status is not cleared as a result of receiving a read receipt.
assert msg_received.get_sender_contact().status == "New status"
ac1.set_config("selfstatus", "")
chat12.send_text("hello")
@@ -2236,26 +2547,31 @@ class TestOnlineAccount:
assert received_reply.quoted_text == "hello"
assert received_reply.quote.id == out_msg.id
@pytest.mark.parametrize("folder,move,expected_destination,", [
("xyz", False, "xyz"), # Test that emails are recognized in a random folder but not moved
("xyz", True, "DeltaChat"), # ...emails are found in a random folder and moved to DeltaChat
("Spam", False, "INBOX") # ...emails are moved from the spam folder to the Inbox
@pytest.mark.parametrize("folder,move,expected_destination,inbox_watch,", [
("xyz", False, "xyz", "1"), # Test that emails are recognized in a random folder but not moved
("xyz", True, "DeltaChat", "1"), # ...emails are found in a random folder and moved to DeltaChat
("Spam", False, "INBOX", "1"), # ...emails are moved from the spam folder to the Inbox
("INBOX", False, "INBOX", "0"), # ...emails are found in the `Inbox` folder even if `inbox_watch` is "0"
])
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
def test_scan_folders(self, acfactory, lp, folder, move, expected_destination):
def test_scan_folders(self, acfactory, lp, folder, move, expected_destination, inbox_watch):
"""Delta Chat periodically scans all folders for new messages to make sure we don't miss any."""
variant = folder + "-" + str(move) + "-" + expected_destination
lp.sec("Testing variant " + variant)
ac1 = acfactory.get_online_configuring_account(move=move)
ac2 = acfactory.get_online_configuring_account()
ac1.set_config("inbox_watch", inbox_watch)
acfactory.wait_configure(ac1)
ac1.direct_imap.create_folder(folder)
acfactory.wait_configure_and_start_io()
# Wait until each folder was selected once and we are IDLEing:
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
if inbox_watch == "1":
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
else:
ac1._evtracker.get_info_contains("IMAP-fake-IDLE: no folder, waiting for interrupt")
ac1.stop_io()
# Send a message to ac1 and move it to the mvbox:
@@ -2272,7 +2588,10 @@ class TestOnlineAccount:
assert msg.text == "hello"
# Wait until the message was moved (if at all) and we are IDLEing again:
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
if inbox_watch == "1":
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
else:
ac1._evtracker.get_info_contains("IMAP-fake-IDLE: no folder, waiting for interrupt")
ac1.direct_imap.select_folder(expected_destination)
assert len(ac1.direct_imap.get_all_messages()) == 1
if folder != expected_destination:
@@ -2518,7 +2837,6 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
def test_invalid_user(self, acfactory):
ac1, configdict = acfactory.get_online_config()
@@ -2526,7 +2844,6 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
def test_invalid_domain(self, acfactory):
ac1, configdict = acfactory.get_online_config()
@@ -2534,4 +2851,3 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")

View File

@@ -1,7 +1,6 @@
[tox]
# make sure to update environment list in travis.yml and appveyor.yml
envlist =
py37
py3
lint
auditwheels
@@ -13,7 +12,6 @@ passenv =
TRAVIS
DCC_RS_DEV
DCC_RS_TARGET
DCC_PY_LIVECONFIG
DCC_NEW_TMP_EMAIL
CARGO_TARGET_DIR
RUSTC_WRAPPER
@@ -47,10 +45,9 @@ commands =
[testenv:doc]
changedir=doc
deps =
# With Python 3.7 and Sphinx 3.5.0, it throws an exception.
# Pin the version to the working one.
# Pin dependencies to the versions which actually work with Python 3.5.
sphinx==3.4.3
breathe
breathe==4.28.0
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html

View File

@@ -1 +1 @@
1.50.0
1.54.0

View File

@@ -1,11 +1,12 @@
# Continuous Integration Scripts for Delta Chat
Continuous Integration, run through CircleCI and an own build machine.
Continuous Integration, run through [GitHub
Actions](https://docs.github.com/actions)
and an own build machine.
## Description of scripts
- `../.circleci/config.yml` describing the build jobs that are run
by Circle-CI
- `../.github/workflows` contains jobs run by GitHub Actions.
- `remote_tests_python.sh` rsyncs to a build machine and runs
`run-python-test.sh` remotely on the build machine.
@@ -26,8 +27,8 @@ There is experimental support for triggering a remote Python or Rust test run
from your local checkout/branch. You will need to be authorized to login to
the build machine (ask your friendly sysadmin on #deltachat freenode) to type::
ci_scripts/manual_remote_tests.sh rust
ci_scripts/manual_remote_tests.sh python
scripts/manual_remote_tests.sh rust
scripts/manual_remote_tests.sh python
This will **rsync** your current checkout to the remote build machine
(no need to commit before) and then run either rust or python tests.
@@ -41,6 +42,10 @@ python tests and build wheels (binary packages for Python)
You can build the docker images yourself locally
to avoid the relatively large download::
cd ci_scripts # where all CI things are
cd scripts # where all CI things are
docker build -t deltachat/coredeps docker-coredeps
docker build -t deltachat/doxygen docker-doxygen
Additionally, you can install qemu and build arm64 docker image:
apt-get install qemu binfmt-support qemu-user-static
docker build -t deltachat/coredeps-arm64 docker-coredeps-arm64

View File

@@ -0,0 +1,21 @@
# Concourse CI pipeline
`docs_wheels.yml` is a pipeline for [Concourse CI](https://concourse-ci.org/)
that builds C documentation, Python documentation, Python wheels for `x86_64`
and `aarch64` and Python source packages, and uploads them.
To setup the pipeline run
```
fly -t <your-target> set-pipeline -c docs_wheels.yml -p docs_wheels -l secret.yml
```
where `secret.yml` contains the following secrets:
```
c.delta.chat:
private_key: |
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
devpi:
login: dc
password: ...
```

View File

@@ -0,0 +1,232 @@
resources:
- name: deltachat-core-rust
type: git
icon: github
source:
branch: master
uri: https://github.com/deltachat/deltachat-core-rust.git
- name: deltachat-core-rust-release
type: git
icon: github
source:
branch: master
uri: https://github.com/deltachat/deltachat-core-rust.git
tag_filter: "py-*"
jobs:
- name: doxygen
plan:
- get: deltachat-core-rust
trigger: true
# Build Doxygen documentation
- task: build-doxygen
config:
inputs:
- name: deltachat-core-rust
outputs:
- name: c-docs
image_resource:
source:
repository: hrektts/doxygen
type: registry-image
platform: linux
run:
path: bash
args:
- -exc
- |
cd deltachat-core-rust
bash scripts/run-doxygen.sh
cd ..
cp -av deltachat-core-rust/deltachat-ffi/{html,xml} c-docs/
- task: upload-c-docs
config:
inputs:
- name: c-docs
image_resource:
type: registry-image
source:
repository: alpine
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache rsync openssh-client
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete c-docs/html/ delta@c.delta.chat:build-c/master
- name: python-x86_64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
repository: vito/oci-build-task
type: registry-image
outputs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/docker-coredeps
UNPACK_ROOTFS: "true"
platform: linux
caches:
- path: cache
run:
path: build
# Use built image to build python wheels
- task: build-wheels
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-docs
path: ./python/doc/_build/
# Source packages
- name: py-dist
path: ./python/.docker-tox/dist/
# Binary wheels
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
platform: linux
run:
path: bash
args:
- -exc
- |
scripts/run_all.sh
# Upload python docs to py.delta.chat
- task: upload-py-docs
config:
inputs:
- name: py-docs
image_resource:
type: registry-image
source:
repository: alpine
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache rsync openssh-client
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete py-docs/html/ delta@py.delta.chat:build/master
# Upload x86_64 wheels and source packages
- task: upload-wheels
config:
inputs:
- name: py-wheels
- name: py-dist
image_resource:
type: registry-image
source:
repository: debian
platform: linux
run:
path: sh
args:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools
pip3 install devpi
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*manylinux201*
devpi upload py-dist/*
- name: python-aarch64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
repository: vito/oci-build-task
type: registry-image
outputs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/docker-coredeps-arm64
UNPACK_ROOTFS: "true"
platform: linux
caches:
- path: cache
run:
path: build
# Use built image to build python wheels
- task: build-wheels
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
platform: linux
run:
path: bash
args:
- -exc
- |
scripts/run_all.sh
# Upload aarch64 wheels
- task: upload-wheels
config:
inputs:
- name: py-wheels
image_resource:
type: registry-image
source:
repository: debian
platform: linux
run:
path: sh
args:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools
pip3 install devpi
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*manylinux201*

26
scripts/coverage.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/sh
set -eu
if ! which grcov 2>/dev/null 1>&2; then
echo >&2 '`grcov` not found. Check README at https://github.com/mozilla/grcov for setup instructions.'
echo >&2 'Run `cargo install grcov` to build `grcov` from source.'
exit 1
fi
# Allow `-Z` flags without using nightly Rust.
export RUSTC_BOOTSTRAP=1
# We are using `-Zprofile` instead of source-based coverage [1]
# (`-Zinstrument-coverage`) due to a bug resulting in empty reports [2].
#
# [1] https://blog.rust-lang.org/inside-rust/2020/11/12/source-based-code-coverage.html
# [2] https://github.com/mozilla/grcov/issues/595
export CARGO_INCREMENTAL=0
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
export RUSTDOCFLAGS="-Cpanic=abort"
cargo clean
cargo build
cargo test
grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing -o ./coverage/

View File

@@ -1,11 +1,11 @@
FROM quay.io/pypa/manylinux2010_x86_64
FROM quay.io/pypa/manylinux2014_aarch64
# Configure ld.so/ldconfig and pkg-config
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
# Install a recent Perl, needed to install the openssl crate
# Install a recent Perl, needed to install the openssl crate
ADD deps/build_perl.sh /builder/build_perl.sh
RUN rm /usr/bin/perl
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1

View File

@@ -1,7 +1,7 @@
#!/bin/bash
PERL_VERSION=5.30.0
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
PERL_VERSION=5.34.0
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
tar -xzf perl-${PERL_VERSION}.tar.gz

View File

@@ -0,0 +1,14 @@
#!/bin/bash
set -x -e
# we use the python3.6 environment as the base environment
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
pushd /usr/bin
ln -s /opt/_internal/cpython-3.6.*/bin/tox
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
popd

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e -x
# Install Rust
#
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.54.0
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
rustc --version
cd ..
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"

View File

@@ -0,0 +1,21 @@
FROM quay.io/pypa/manylinux2014_x86_64
# Configure ld.so/ldconfig and pkg-config
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
# Install a recent Perl, needed to install the openssl crate
ADD deps/build_perl.sh /builder/build_perl.sh
RUN rm /usr/bin/perl
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
# Install python tools (auditwheels,tox, ...)
ADD deps/build_python.sh /builder/build_python.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
# Install Rust
ADD deps/build_rust.sh /builder/build_rust.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1

View File

@@ -0,0 +1,21 @@
#!/bin/bash
set -e -x
OPENSSL_VERSION=1.1.1a
OPENSSL_SHA256=fc20130f8b7cbd2fb918b2f14e2f429e109c31ddd0fb38fc5d71d9ffed3f9f41
curl -O https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz
echo "${OPENSSL_SHA256} openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c -
tar xzf openssl-${OPENSSL_VERSION}.tar.gz
cd openssl-${OPENSSL_VERSION}
./config shared no-ssl2 no-ssl3 -fPIC --prefix=/usr/local
sed -i "s/^SHLIB_MAJOR=.*/SHLIB_MAJOR=200/" Makefile && \
sed -i "s/^SHLIB_MINOR=.*/SHLIB_MINOR=0.0/" Makefile && \
sed -i "s/^SHLIB_VERSION_NUMBER=.*/SHLIB_VERSION_NUMBER=200.0.0/" Makefile
make depend
make
make install_sw install_ssldirs
ldconfig -v | grep ssl

View File

@@ -0,0 +1,12 @@
#!/bin/bash
PERL_VERSION=5.34.0
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
tar -xzf perl-${PERL_VERSION}.tar.gz
cd perl-${PERL_VERSION}
./Configure -de
make
make install

View File

@@ -0,0 +1,14 @@
#!/bin/bash
set -x -e
# we use the python3.6 environment as the base environment
/opt/python/cp36-cp36m/bin/pip install tox devpi-client auditwheel
pushd /usr/bin
ln -s /opt/_internal/cpython-3.6.*/bin/tox
ln -s /opt/_internal/cpython-3.6.*/bin/devpi
ln -s /opt/_internal/cpython-3.6.*/bin/auditwheel
popd

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e -x
# Install Rust
#
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.54.0
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
rustc --version
cd ..
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"

8
scripts/manual_remote_tests.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
set -xe
JOB=${1:?need to specify 'rust' or 'python'}
BRANCH="$(git branch | grep \* | cut -d ' ' -f2)"
REPONAME="$(basename $(git rev-parse --show-toplevel))"
time bash "scripts/remote_tests_$JOB.sh" "$USER-$BRANCH-$REPONAME"

View File

@@ -1,10 +1,9 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:-master}
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
BUILD_ID=${1:?specify build ID}
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
BUILDDIR=ci_builds/$BUILD_ID
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
@@ -18,7 +17,7 @@ rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
set +x
echo "--- Running $CIRCLE_JOB remotely"
echo "--- Running Python tests remotely"
ssh $SSHTARGET <<_HERE
set +x -e
@@ -29,7 +28,6 @@ ssh $SSHTARGET <<_HERE
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
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
@@ -43,5 +41,5 @@ ssh $SSHTARGET <<_HERE
source \$HOME/venv/bin/activate
which python
bash ci_scripts/run-python-test.sh
bash scripts/run-python-test.sh
_HERE

29
scripts/remote_tests_rust.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
BUILD_ID=${1:?specify build ID}
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
BUILDDIR=ci_builds/$BUILD_ID
set -e
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
git ls-files >.rsynclist
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
echo "--- Running Rust tests remotely"
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
export TARGET=x86_64-unknown-linux-gnu
export RUSTC_WRAPPER=sccache
bash scripts/run-rust-test.sh
_HERE

6
scripts/run-doxygen.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -ex
cd deltachat-ffi
PROJECT_NUMBER=$(git log -1 --format="%h (%cd)") doxygen

View File

@@ -4,11 +4,11 @@
# purposes. Any arguments are passed straight to tox. E.g. to run
# only one environment run with:
#
# ./run-integration-tests.sh -e py35
# scripts/run-integration-tests.sh -e py35
#
# To also run with `pytest -x` use:
#
# ./run-integration-tests.sh -e py35 -- -x
# scripts/run-integration-tests.sh -e py35 -- -x
export DCC_RS_DEV=$(pwd)
export DCC_RS_TARGET=${DCC_RS_TARGET:-release}
@@ -23,9 +23,6 @@ if [ $? != 0 ]; then
fi
pushd python
if [ -e "./liveconfig" -a -z "$DCC_PY_LIVECONFIG" ]; then
export DCC_PY_LIVECONFIG=liveconfig
fi
tox "$@"
ret=$?
popd

View File

@@ -19,14 +19,17 @@ export DCC_RS_TARGET=release
# Configure access to a base python and to several python interpreters
# needed by tox below.
export PATH=$PATH:/opt/python/cp35-cp35m/bin
export PATH=$PATH:/opt/python/cp36-cp36m/bin
export PYTHONDONTWRITEBYTECODE=1
pushd /bin
rm -f python3.5
ln -s /opt/python/cp35-cp35m/bin/python3.5
rm -f python3.6
ln -s /opt/python/cp36-cp36m/bin/python3.6
rm -f python3.7
ln -s /opt/python/cp37-cp37m/bin/python3.7
rm -f python3.8
ln -s /opt/python/cp38-cp38/bin/python3.8
rm -f python3.9
ln -s /opt/python/cp39-cp39/bin/python3.9
popd
pushd python
@@ -39,9 +42,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_NEW_TMP_EMAIL
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,auditwheels
popd

View File

@@ -82,9 +82,10 @@ def main():
subprocess.call(["git", "add", "-u"])
# subprocess.call(["cargo", "update", "-p", "deltachat"])
print("after commit make sure to: ")
print("after commit, on master make sure to: ")
print("")
print(" git tag {}".format(newversion))
print(" git tag -a {}".format(newversion))
print(" git push origin {}".format(newversion))
print("")

63
spec.md
View File

@@ -1,6 +1,6 @@
# chat-mail specification
Version: 0.32.0
Version: 0.33.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
@@ -301,9 +301,9 @@ to add a `Chat-Group-Avatar` only on image changes.
A user MAY have a profile-image that MAY be distributed to their contacts.
To change or set the profile-image,
the messenger MUST attach an image file to a message
and MUST add the header `Chat-User-Avatar`
with the value set to the image name.
the messenger MUST add the header `Chat-User-Avatar: base64:IMAGEDATA`.
To bypass limits of headers, it is recommended not to use the outer header
and to limit the size to 20k.
To remove the profile-image,
the messenger MUST add the header `Chat-User-Avatar: 0`.
@@ -320,19 +320,14 @@ The messenger SHOULD NOT send an explicit mail to normal MUAs.
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Chat-User-Avatar: photo.jpg
Subject: Chat: Hello, ...
Content-Type: multipart/mixed; boundary="==break=="
--==break==
Content-Type: text/plain
Chat-User-Avatar: base64:AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQY ...
Hello, I've changed my profile image.
--==break==
Content-Type: image/jpeg
Content-Disposition: attachment; filename="photo.jpg"
AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQYFBA ...
--==break==--
The image format SHOULD be image/jpeg or image/png.
@@ -342,6 +337,11 @@ in the same message.
To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
only on image changes.
In older specs, the profile-image was sent as an attachment
and `Chat-User-Avatar:` specified its name.
However, it turned out that these attachements are kind of unuexpected to users,
therefore the profile-image go to the header now.
# Locations
@@ -401,9 +401,41 @@ it is fine if the location is detected on forwarding etc.
</kml>
# Miscellaneous
# Stickers
Messengers SHOULD use the header `In-Reply-To` as usual.
Stickers are send as normal images
with the additional header `Chat-Content: sticker`.
It is discouraged to send stickers together with user generated text,
however, stickers can be used as a reply to a message
and also the footer should be set as usual.
From: alice@example.org
To: bob@example.com
Chat-Version: 1.0
Chat-Content: sticker
Message-ID: Mr.12345uvwxyZ.0005@example.org
Subject: Message from Alice
Content-Type: multipart/mixed; boundary="==break=="
--==break==
Content-Type: text/plain
--
Hi there! I am using this new messenger!
--==break==
Content-Type: image/png
Content-Disposition: attachment; filename="sticker.png"
R0lGODlhpAGkAfe9AP+zd2eQkZhrI//z9v++PMb///+scrdDT3BtbtrZ2f/LQSsREcdIVf9 ...
--==break==--
Typical sticker formats are `image/png`, `image/gif` and `image/webp`.
Animated stickers are supported
by just using an image format that supports animation.
# Voice messages
Messengers SHOULD add a `Chat-Voice-message: 1` header
if an attached audio file is a voice message.
@@ -417,6 +449,11 @@ This allows the receiver to show the time without knowing the file format.
Chat-Voice-Message: 1
Chat-Duration: 10000
# Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual.
Messengers MAY send and receive Message Disposition Notifications
(MDNs, [RFC 8098](https://tools.ietf.org/html/rfc8098),
[RFC 3503](https://tools.ietf.org/html/rfc3503))
@@ -437,4 +474,4 @@ as the sending time of the message as indicated by its Date header,
or the time of first receipt if that date is in the future or unavailable.
Copyright © 2017-2020 Delta Chat contributors.
Copyright © 2017-2021 Delta Chat contributors.

View File

@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use async_std::channel::{Receiver, Sender};
use async_std::fs;
use async_std::path::PathBuf;
use async_std::prelude::*;
@@ -18,6 +19,7 @@ pub struct Accounts {
dir: PathBuf,
config: Config,
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
emitter: EventEmitter,
}
impl Accounts {
@@ -30,19 +32,13 @@ impl Accounts {
Accounts::open(dir).await
}
/// Creates a new default structure, including a default account.
/// Creates a new default structure.
pub async fn create(os_name: String, dir: &PathBuf) -> Result<()> {
fs::create_dir_all(dir)
.await
.context("failed to create folder")?;
// create default account
let config = Config::new(os_name.clone(), dir).await?;
let account_config = config.new_account(dir).await?;
Context::new(os_name, account_config.dbfile().into(), account_config.id)
.await
.context("failed to create default account")?;
Config::new(os_name.clone(), dir).await?;
Ok(())
}
@@ -58,10 +54,16 @@ impl Accounts {
let config = Config::from_file(config_file).await?;
let accounts = config.load_accounts().await?;
let emitter = EventEmitter::new();
for account in accounts.values() {
emitter.add_account(account).await?;
}
Ok(Self {
dir,
config,
accounts: Arc::new(RwLock::new(accounts)),
emitter,
})
}
@@ -71,14 +73,17 @@ impl Accounts {
}
/// Get the currently selected account.
pub async fn get_selected_account(&self) -> Context {
pub async fn get_selected_account(&self) -> Option<Context> {
let id = self.config.get_selected_account().await;
self.accounts
.read()
.await
.get(&id)
.cloned()
.expect("inconsistent state")
self.accounts.read().await.get(&id).cloned()
}
/// Returns the currently selected account's id or None if no account is selected.
pub async fn get_selected_account_id(&self) -> Option<u32> {
match self.config.get_selected_account().await {
0 => None,
id => Some(id),
}
}
/// Select the given account.
@@ -94,6 +99,7 @@ impl Accounts {
let account_config = self.config.new_account(&self.dir).await?;
let ctx = Context::new(os_name, account_config.dbfile().into(), account_config.id).await?;
self.emitter.add_account(&ctx).await?;
self.accounts.write().await.insert(account_config.id, ctx);
Ok(account_config.id)
@@ -120,6 +126,7 @@ impl Accounts {
/// Migrate an existing account into this structure.
pub async fn migrate_account(&self, dbfile: PathBuf) -> Result<u32> {
let blobdir = Context::derive_blobdir(&dbfile);
let walfile = Context::derive_walfile(&dbfile);
ensure!(
dbfile.exists().await,
@@ -135,15 +142,31 @@ impl Accounts {
let old_id = self.config.get_selected_account().await;
// create new account
let account_config = self.config.new_account(&self.dir).await?;
let account_config = self
.config
.new_account(&self.dir)
.await
.context("failed to create new account")?;
let new_dbfile = account_config.dbfile().into();
let new_blobdir = Context::derive_blobdir(&new_dbfile);
let new_walfile = Context::derive_walfile(&new_dbfile);
let res = {
fs::create_dir_all(&account_config.dir).await?;
fs::rename(&dbfile, &new_dbfile).await?;
fs::rename(&blobdir, &new_blobdir).await?;
fs::create_dir_all(&account_config.dir)
.await
.context("failed to create dir")?;
fs::rename(&dbfile, &new_dbfile)
.await
.context("failed to rename dbfile")?;
fs::rename(&blobdir, &new_blobdir)
.await
.context("failed to rename blobdir")?;
if walfile.exists().await {
fs::rename(&walfile, &new_walfile)
.await
.context("failed to rename walfile")?;
}
Ok(())
};
@@ -156,6 +179,7 @@ impl Accounts {
account_config.id,
)
.await?;
self.emitter.add_account(&ctx).await?;
self.accounts.write().await.insert(account_config.id, ctx);
Ok(account_config.id)
}
@@ -180,23 +204,23 @@ impl Accounts {
self.accounts.read().await.keys().copied().collect()
}
/// Import a backup using a new account and selects it.
pub async fn import_account(&self, file: PathBuf) -> Result<u32> {
let old_id = self.config.get_selected_account().await;
let id = self.add_account().await?;
let ctx = self.get_account(id).await.expect("just added");
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, &file).await {
Ok(_) => Ok(id),
Err(err) => {
// remove temp account
self.remove_account(id).await?;
// set selection back
self.select_account(old_id).await?;
Err(err)
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
///
/// Returns whether all accounts finished their background work.
/// DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
///
/// iOS can:
/// - call dc_start_io() (in case IO was not running)
/// - call dc_maybe_network()
/// - while dc_accounts_all_work_done() returns false:
/// - Wait for DC_EVENT_CONNECTIVITY_CHANGED
pub async fn all_work_done(&self) -> bool {
for account in self.accounts.read().await.values() {
if !account.all_work_done().await {
return false;
}
}
true
}
pub async fn start_io(&self) {
@@ -220,32 +244,69 @@ impl Accounts {
}
}
pub async fn maybe_network_lost(&self) {
let accounts = &*self.accounts.read().await;
for account in accounts.values() {
account.maybe_network_lost().await;
}
}
/// Unified event emitter.
pub async fn get_event_emitter(&self) -> EventEmitter {
let emitters: Vec<_> = self
.accounts
.read()
.await
.iter()
.map(|(_id, a)| a.get_event_emitter())
.collect();
EventEmitter(futures::stream::select_all(emitters))
self.emitter.clone()
}
}
#[derive(Debug)]
pub struct EventEmitter(futures::stream::SelectAll<crate::events::EventEmitter>);
#[derive(Debug, Clone)]
pub struct EventEmitter {
/// Aggregate stream of events from all accounts.
stream: Arc<RwLock<futures::stream::SelectAll<crate::events::EventEmitter>>>,
/// Sender for the channel where new account emitters will be pushed.
sender: Sender<crate::events::EventEmitter>,
/// Receiver for the channel where new account emitters will be pushed.
receiver: Receiver<crate::events::EventEmitter>,
}
impl EventEmitter {
/// Blocking recv of an event. Return `None` if all `Sender`s have been droped.
pub fn recv_sync(&mut self) -> Option<Event> {
async_std::task::block_on(self.recv())
pub fn new() -> Self {
let (sender, receiver) = async_std::channel::unbounded();
Self {
stream: Arc::new(RwLock::new(futures::stream::SelectAll::new())),
sender,
receiver,
}
}
/// Async recv of an event. Return `None` if all `Sender`s have been droped.
pub async fn recv(&mut self) -> Option<Event> {
self.0.next().await
/// Blocking recv of an event. Return `None` if all `Sender`s have been droped.
pub fn recv_sync(&mut self) -> Option<Event> {
async_std::task::block_on(self.recv()).unwrap_or_default()
}
/// Async recv of an event. Return `None` if all `Sender`s have been dropped.
pub async fn recv(&mut self) -> Result<Option<Event>> {
let mut stream = self.stream.write().await;
loop {
match futures::future::select(self.receiver.recv(), stream.next()).await {
futures::future::Either::Left((emitter, _)) => {
stream.push(emitter?);
}
futures::future::Either::Right((ev, _)) => return Ok(ev),
}
}
}
/// Add event emitter of a new account to the aggregate event emitter.
pub async fn add_account(&self, context: &Context) -> Result<()> {
self.sender.send(context.get_event_emitter()).await?;
Ok(())
}
}
impl Default for EventEmitter {
fn default() -> Self {
Self::new()
}
}
@@ -256,7 +317,7 @@ impl async_std::stream::Stream for EventEmitter {
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self.0).poll_next(cx)
std::pin::Pin::new(&mut self).poll_next(cx)
}
}
@@ -434,6 +495,8 @@ mod tests {
let p: PathBuf = dir.path().join("accounts1").into();
let accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
accounts1.add_account().await.unwrap();
let accounts2 = Accounts::open(p).await.unwrap();
assert_eq!(accounts1.accounts.read().await.len(), 1);
@@ -456,7 +519,11 @@ mod tests {
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 0);
assert_eq!(accounts.config.get_selected_account().await, 0);
let id = accounts.add_account().await.unwrap();
assert_eq!(id, 1);
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
@@ -473,14 +540,35 @@ mod tests {
assert_eq!(accounts.accounts.read().await.len(), 1);
}
#[async_std::test]
async fn test_accounts_remove_last() -> Result<()> {
let dir = tempfile::tempdir()?;
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
assert!(accounts.get_selected_account().await.is_none());
assert_eq!(accounts.config.get_selected_account().await, 0);
let id = accounts.add_account().await?;
assert!(accounts.get_selected_account().await.is_some());
assert_eq!(id, 1);
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, id);
accounts.remove_account(id).await?;
assert!(accounts.get_selected_account().await.is_none());
Ok(())
}
#[async_std::test]
async fn test_migrate_account() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
assert_eq!(accounts.accounts.read().await.len(), 0);
assert_eq!(accounts.config.get_selected_account().await, 0);
let extern_dbfile: PathBuf = dir.path().join("other").into();
let ctx = Context::new("my_os".into(), extern_dbfile.clone(), 0)
@@ -496,13 +584,16 @@ mod tests {
.migrate_account(extern_dbfile.clone())
.await
.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 2);
assert_eq!(accounts.config.get_selected_account().await, 2);
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
let ctx = accounts.get_selected_account().await;
let ctx = accounts.get_selected_account().await.unwrap();
assert_eq!(
"me@mail.com",
ctx.get_config(crate::config::Config::Addr).await.unwrap()
ctx.get_config(crate::config::Config::Addr)
.await
.unwrap()
.unwrap()
);
}
@@ -514,7 +605,7 @@ mod tests {
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
for expected_id in 2..10 {
for expected_id in 1..10 {
let id = accounts.add_account().await.unwrap();
assert_eq!(id, expected_id);
}
@@ -524,4 +615,86 @@ mod tests {
assert_eq!(ids.get(i), Some(&expected_id));
}
}
#[async_std::test]
async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
let dir = tempfile::tempdir()?;
let p: PathBuf = dir.path().join("accounts").into();
let dummy_accounts = 10;
let (id0, id1, id2) = {
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
accounts.add_account().await?;
let ids = accounts.get_all().await;
assert_eq!(ids.len(), 1);
let id0 = *ids.get(0).unwrap();
let ctx = accounts.get_account(id0).await.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
.await?;
let id1 = accounts.add_account().await?;
let ctx = accounts.get_account(id1).await.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
.await?;
// add and remove some accounts and force a gap (ids must not be reused)
for _ in 0..dummy_accounts {
let to_delete = accounts.add_account().await?;
accounts.remove_account(to_delete).await?;
}
let id2 = accounts.add_account().await?;
let ctx = accounts.get_account(id2).await.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
.await?;
accounts.select_account(id1).await?;
(id0, id1, id2)
};
assert!(id0 > 0);
assert!(id1 > id0);
assert!(id2 > id1 + dummy_accounts);
let (id0_reopened, id1_reopened, id2_reopened) = {
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
let ctx = accounts.get_selected_account().await.unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("two@example.org".to_string())
);
let ids = accounts.get_all().await;
assert_eq!(ids.len(), 3);
let id0 = *ids.get(0).unwrap();
let ctx = accounts.get_account(id0).await.unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("one@example.org".to_string())
);
let id1 = *ids.get(1).unwrap();
let t = accounts.get_account(id1).await.unwrap();
assert_eq!(
t.get_config(crate::config::Config::Addr).await?,
Some("two@example.org".to_string())
);
let id2 = *ids.get(2).unwrap();
let ctx = accounts.get_account(id2).await.unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("three@example.org".to_string())
);
(id0, id1, id2)
};
assert_eq!(id0, id0_reopened);
assert_eq!(id1, id1_reopened);
assert_eq!(id2, id2_reopened);
Ok(())
}
}

View File

@@ -1,5 +1,6 @@
//! # Blob directory management
use core::cmp::max;
use std::ffi::OsStr;
use std::fmt;
@@ -7,8 +8,12 @@ use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use anyhow::format_err;
use anyhow::Context as _;
use anyhow::Error;
use image::DynamicImage;
use image::GenericImageView;
use image::ImageFormat;
use num_traits::FromPrimitive;
use thiserror::Error;
@@ -53,11 +58,11 @@ impl<'a> BlobObject<'a> {
/// underlying error.
pub async fn create(
context: &'a Context,
suggested_name: impl AsRef<str>,
suggested_name: &str,
data: &[u8],
) -> std::result::Result<BlobObject<'a>, BlobError> {
let blobdir = context.get_blobdir();
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
let (stem, ext) = BlobObject::sanitise_name(suggested_name);
let (name, mut file) = BlobObject::create_new_file(blobdir, &stem, &ext).await?;
file.write_all(data)
.await
@@ -69,7 +74,7 @@ impl<'a> BlobObject<'a> {
// workaround a bug in async-std
// (the executor does not handle blocking operation in Drop correctly,
// see https://github.com/async-rs/async-std/issues/900 )
// see <https://github.com/async-rs/async-std/issues/900>)
let _ = file.flush().await;
let blob = BlobObject {
@@ -132,18 +137,17 @@ impl<'a> BlobObject<'a> {
/// copied.
pub async fn create_and_copy(
context: &'a Context,
src: impl AsRef<Path>,
src: &Path,
) -> std::result::Result<BlobObject<'a>, BlobError> {
let mut src_file =
fs::File::open(src.as_ref())
.await
.map_err(|err| BlobError::CopyFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: String::from(""),
src: src.as_ref().to_path_buf(),
cause: err,
})?;
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
let mut src_file = fs::File::open(src)
.await
.map_err(|err| BlobError::CopyFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: String::from(""),
src: src.to_path_buf(),
cause: err,
})?;
let (stem, ext) = BlobObject::sanitise_name(&src.to_string_lossy());
let (name, mut dst_file) =
BlobObject::create_new_file(context.get_blobdir(), &stem, &ext).await?;
let name_for_err = name.clone();
@@ -156,7 +160,7 @@ impl<'a> BlobObject<'a> {
return Err(BlobError::CopyFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: name_for_err,
src: src.as_ref().to_path_buf(),
src: src.to_path_buf(),
cause: err,
});
}
@@ -190,16 +194,13 @@ impl<'a> BlobObject<'a> {
/// the [BlobObject::from_path] methods. See those for possible
/// errors.
pub async fn new_from_path(
context: &Context,
src: impl AsRef<Path>,
) -> std::result::Result<BlobObject<'_>, BlobError> {
if src.as_ref().starts_with(context.get_blobdir()) {
context: &'a Context,
src: &Path,
) -> std::result::Result<BlobObject<'a>, BlobError> {
if src.starts_with(context.get_blobdir()) {
BlobObject::from_path(context, src)
} else if src.as_ref().starts_with("$BLOBDIR/") {
BlobObject::from_name(
context,
src.as_ref().to_str().unwrap_or_default().to_string(),
)
} else if src.starts_with("$BLOBDIR/") {
BlobObject::from_name(context, src.to_str().unwrap_or_default().to_string())
} else {
BlobObject::create_and_copy(context, src).await
}
@@ -220,23 +221,22 @@ impl<'a> BlobObject<'a> {
/// [BlobError::WrongName] is used if the file name does not
/// remain identical after sanitisation.
pub fn from_path(
context: &Context,
path: impl AsRef<Path>,
) -> std::result::Result<BlobObject, BlobError> {
let rel_path = path
.as_ref()
.strip_prefix(context.get_blobdir())
.map_err(|_| BlobError::WrongBlobdir {
blobdir: context.get_blobdir().to_path_buf(),
src: path.as_ref().to_path_buf(),
})?;
if !BlobObject::is_acceptible_blob_name(&rel_path) {
context: &'a Context,
path: &Path,
) -> std::result::Result<BlobObject<'a>, BlobError> {
let rel_path =
path.strip_prefix(context.get_blobdir())
.map_err(|_| BlobError::WrongBlobdir {
blobdir: context.get_blobdir().to_path_buf(),
src: path.to_path_buf(),
})?;
if !BlobObject::is_acceptible_blob_name(rel_path) {
return Err(BlobError::WrongName {
blobname: path.as_ref().to_path_buf(),
blobname: path.to_path_buf(),
});
}
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
blobname: path.as_ref().to_path_buf(),
blobname: path.to_path_buf(),
})?;
BlobObject::from_name(context, name.to_string())
}
@@ -380,18 +380,26 @@ impl<'a> BlobObject<'a> {
true
}
pub async fn recode_to_avatar_size(&self, context: &Context) -> Result<(), BlobError> {
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<(), BlobError> {
let blob_abs = self.to_abs_path();
let img_wh =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
MediaQuality::Worse => WORSE_AVATAR_SIZE,
};
self.recode_to_size(context, blob_abs, img_wh).await
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
if let Some(new_name) = self
.recode_to_size(context, blob_abs, img_wh, Some(20_000))
.await?
{
self.name = new_name;
}
Ok(())
}
pub async fn recode_to_image_size(&self, context: &Context) -> Result<(), BlobError> {
@@ -403,37 +411,76 @@ impl<'a> BlobObject<'a> {
}
let img_wh =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
MediaQuality::Worse => WORSE_IMAGE_SIZE,
};
self.recode_to_size(context, blob_abs, img_wh).await
if self
.recode_to_size(context, blob_abs, img_wh, None)
.await?
.is_some()
{
return Err(format_err!(
"Internal error: recode_to_size(..., None) shouldn't change the name of the image"
)
.into());
}
Ok(())
}
async fn recode_to_size(
&self,
context: &Context,
blob_abs: PathBuf,
img_wh: u32,
) -> Result<(), BlobError> {
mut blob_abs: PathBuf,
mut img_wh: u32,
max_bytes: Option<usize>,
) -> Result<Option<String>, BlobError> {
let mut img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
})?;
let orientation = self.get_exif_orientation(context);
let mut encoded = Vec::new();
let mut changed_name = None;
let do_scale = img.width() > img_wh || img.height() > img_wh;
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
encoded.clear();
img.write_to(encoded, image::ImageFormat::Jpeg)?;
Ok(())
}
fn encoded_img_exceeds_bytes(
context: &Context,
img: &DynamicImage,
max_bytes: Option<usize>,
encoded: &mut Vec<u8>,
) -> anyhow::Result<bool> {
if let Some(max_bytes) = max_bytes {
encode_img(img, encoded)?;
if encoded.len() > max_bytes {
info!(
context,
"image size {}B ({}x{}px) exceeds {}B, need to scale down",
encoded.len(),
img.width(),
img.height(),
max_bytes,
);
return Ok(true);
}
}
Ok(false)
}
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
let do_scale =
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
if do_scale || do_rotate {
if do_scale {
img = img.thumbnail(img_wh, img_wh);
}
if do_rotate {
img = match orientation {
Ok(90) => img.rotate90(),
@@ -443,14 +490,64 @@ impl<'a> BlobObject<'a> {
}
}
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err.into(),
})?;
if do_scale {
if !exceeds_width {
// The image is already smaller than img_wh, but exceeds max_bytes
// We can directly start with trying to scale down to 2/3 of its current width
img_wh = max(img.width(), img.height()) * 2 / 3
}
loop {
let new_img = img.thumbnail(img_wh, img_wh);
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
if img_wh < 20 {
return Err(format_err!(
"Failed to scale image to below {}B",
max_bytes.unwrap_or_default()
)
.into());
}
img_wh = img_wh * 2 / 3;
} else {
if encoded.is_empty() {
encode_img(&new_img, &mut encoded)?;
}
info!(
context,
"Final scaled-down image size: {}B ({}px)",
encoded.len(),
img_wh
);
break;
}
}
}
// The file format is JPEG now, we may have to change the file extension
if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
blob_abs = blob_abs.with_extension("jpg");
let file_name = blob_abs.file_name().context("No avatar file name (???)")?;
let file_name = file_name.to_str().context("Filename is no UTF-8 (???)")?;
changed_name = Some(format!("$BLOBDIR/{}", file_name));
}
if encoded.is_empty() {
encode_img(&img, &mut encoded)?;
}
fs::write(&blob_abs, &encoded)
.await
.map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err.into(),
})?;
}
Ok(())
Ok(changed_name)
}
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32, Error> {
@@ -514,13 +611,18 @@ pub enum BlobError {
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
#[error("Blob has a badname {}", .blobname.display())]
WrongName { blobname: PathBuf },
#[error("{0}")]
Other(#[from] anyhow::Error),
}
#[cfg(test)]
mod tests {
use fs::File;
use super::*;
use crate::test_utils::TestContext;
use crate::{message::Message, test_utils::TestContext};
use image::Pixel;
#[async_std::test]
async fn test_create() {
@@ -622,13 +724,15 @@ mod tests {
let t = TestContext::new().await;
let src = t.dir.path().join("src");
fs::write(&src, b"boo").await.unwrap();
let blob = BlobObject::create_and_copy(&t, &src).await.unwrap();
let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/src");
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
let whoops = t.dir.path().join("whoops");
assert!(BlobObject::create_and_copy(&t, &whoops).await.is_err());
assert!(BlobObject::create_and_copy(&t, whoops.as_ref())
.await
.is_err());
let whoops = t.get_blobdir().join("whoops");
assert!(!whoops.exists().await);
}
@@ -639,7 +743,9 @@ mod tests {
let src_ext = t.dir.path().join("external");
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t, &src_ext).await.unwrap();
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
.await
.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/external");
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
@@ -656,7 +762,9 @@ mod tests {
let t = TestContext::new().await;
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t, &src_ext).await.unwrap();
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
.await
.unwrap();
assert_eq!(
blob.as_name(),
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
@@ -711,4 +819,249 @@ mod tests {
assert!(!stem.contains('*'));
assert!(!stem.contains('?'));
}
#[async_std::test]
async fn test_selfavatar_outside_blobdir() {
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.jpg");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
File::create(&avatar_src)
.await
.unwrap()
.write_all(avatar_bytes)
.await
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists().await);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists().await);
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap();
assert_eq!(img.width(), 1000);
assert_eq!(img.height(), 1000);
let img = image::open(&avatar_blob).unwrap();
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
async fn file_size(path_buf: &PathBuf) -> u64 {
let file = File::open(path_buf).await.unwrap();
file.metadata().await.unwrap().len()
}
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000))
.await
.unwrap();
assert!(file_size(&avatar_blob).await <= 3000);
assert!(file_size(&avatar_blob).await > 2000);
let img = image::open(&avatar_blob).unwrap();
assert!(img.width() > 130);
assert_eq!(img.width(), img.height());
}
#[async_std::test]
async fn test_selfavatar_in_blobdir() {
let t = TestContext::new().await;
let avatar_src = t.get_blobdir().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
File::create(&avatar_src)
.await
.unwrap()
.write_all(avatar_bytes)
.await
.unwrap();
let img = image::open(&avatar_src).unwrap();
assert_eq!(img.width(), 900);
assert_eq!(img.height(), 900);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
assert_eq!(
avatar_cfg,
avatar_src.with_extension("jpg").to_str().unwrap()
);
let img = image::open(avatar_cfg).unwrap();
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
}
#[async_std::test]
async fn test_selfavatar_copy_without_recode() {
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
File::create(&avatar_src)
.await
.unwrap()
.write_all(avatar_bytes)
.await
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.png");
assert!(!avatar_blob.exists().await);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists().await);
assert_eq!(
std::fs::metadata(&avatar_blob).unwrap().len(),
avatar_bytes.len() as u64
);
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
}
#[async_std::test]
async fn test_recode_image() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
.await
.unwrap();
send_image_check_mediaquality(
Some("1"),
bytes,
1000,
1000,
0,
WORSE_IMAGE_SIZE,
WORSE_IMAGE_SIZE,
)
.await
.unwrap();
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
let img_rotated = send_image_check_mediaquality(
Some("0"),
bytes,
2000,
1800,
270,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let mut bytes = vec![];
img_rotated
.write_to(&mut bytes, image::ImageFormat::Jpeg)
.unwrap();
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let img_rotated = send_image_check_mediaquality(
Some("1"),
&bytes,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
WORSE_IMAGE_SIZE * 1800 / 2000,
WORSE_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
}
fn assert_correct_rotation(img: &DynamicImage) {
// The test images are black in the bottom left corner after correctly applying
// the EXIF orientation
let [luma] = img.get_pixel(10, 10).to_luma().0;
assert_eq!(luma, 255);
let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
assert_eq!(luma, 255);
let [luma] = img
.get_pixel(img.width() - 10, img.height() - 10)
.to_luma()
.0;
assert_eq!(luma, 255);
let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
assert_eq!(luma, 0);
}
async fn send_image_check_mediaquality(
media_quality_config: Option<&str>,
bytes: &[u8],
original_width: u32,
original_height: u32,
orientation: i32,
compressed_width: u32,
compressed_height: u32,
) -> anyhow::Result<DynamicImage> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice
.set_config(Config::MediaQuality, media_quality_config)
.await?;
let file = alice.get_blobdir().join("file.jpg");
File::create(&file).await?.write_all(bytes).await?;
let img = image::open(&file)?;
assert_eq!(img.width(), original_width);
assert_eq!(img.height(), original_height);
let blob = BlobObject::new_from_path(&alice, &file).await?;
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let chat = alice.create_chat(&bob).await;
let sent = alice.send_msg(chat.id, &mut msg).await;
let alice_msg = alice.get_last_msg().await;
assert_eq!(alice_msg.get_width() as u32, compressed_width);
assert_eq!(alice_msg.get_height() as u32, compressed_height);
let img = image::open(alice_msg.get_file(&alice).unwrap())?;
assert_eq!(img.width() as u32, compressed_width);
assert_eq!(img.height() as u32, compressed_height);
bob.recv_msg(&sent).await;
let bob_msg = bob.get_last_msg().await;
assert_eq!(bob_msg.get_width() as u32, compressed_width);
assert_eq!(bob_msg.get_height() as u32, compressed_height);
let file = bob_msg.get_file(&bob).unwrap();
let blob = BlobObject::new_from_path(&bob, &file).await?;
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
let img = image::open(file)?;
assert_eq!(img.width() as u32, compressed_width);
assert_eq!(img.height() as u32, compressed_height);
Ok(img)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,11 @@
use anyhow::{bail, ensure, Result};
use crate::chat;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_DEADDROP,
DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT,
DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CONTACT_ID_DEVICE,
DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT, DC_GCL_ARCHIVED_ONLY,
DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
};
use crate::contact::Contact;
use crate::context::Context;
@@ -35,15 +34,12 @@ use crate::stock_str;
/// and for each messages that is scrolled into view, dc_get_msg() is called then.
///
/// Why no listflags?
/// Without listflags, dc_get_chatlist() adds the deaddrop and the archive "link" automatically as needed.
/// The UI can just render these items differently then. Although the deaddrop link is currently always the
/// first entry and only present on new messages, there is the rough idea that it can be optionally always
/// present and sorted into the list by date. Rendering the deaddrop in the described way
/// would not add extra work in the UI then.
/// Without listflags, dc_get_chatlist() adds the archive "link" automatically as needed.
/// The UI can just render these items differently then.
#[derive(Debug)]
pub struct Chatlist {
/// Stores pairs of `chat_id, message_id`
ids: Vec<(ChatId, MsgId)>,
ids: Vec<(ChatId, Option<MsgId>)>,
}
impl Chatlist {
@@ -59,12 +55,6 @@ impl Chatlist {
///
/// By default, the function adds some special entries to the list.
/// These special entries can be identified by the ID returned by chatlist.get_chat_id():
/// - DC_CHAT_ID_DEADDROP (1) - this special chat is present if there are
/// messages from addresses that have no relationship to the configured account.
/// The last of these messages is represented by DC_CHAT_ID_DEADDROP and you can retrieve details
/// about it with chatlist.get_msg_id(). Typically, the UI asks the user "Do you want to chat with NAME?"
/// and offers the options "Start chat", "Block" and "Not now";
/// The decision should be passed to dc_decide_on_contact_request().
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
/// archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
/// "Show archived chats", if the user clicks this item, the UI should show a
@@ -80,9 +70,9 @@ impl Chatlist {
/// 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,
/// and hides the device-chat and contact requests
/// 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
/// - if the flag DC_GCL_NO_SPECIALS is set, archive link is 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)
/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
@@ -112,7 +102,7 @@ impl Chatlist {
let process_row = |row: &rusqlite::Row| {
let chat_id: ChatId = row.get(0)?;
let msg_id: MsgId = row.get(1).unwrap_or_default();
let msg_id: Option<MsgId> = row.get(1)?;
Ok((chat_id, msg_id))
};
@@ -122,10 +112,9 @@ impl Chatlist {
};
let skip_id = if flag_for_forwarding {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.await
ChatId::lookup_by_contact(context, DC_CONTACT_ID_DEVICE)
.await?
.unwrap_or_default()
.0
} else {
ChatId::new(0)
};
@@ -138,13 +127,8 @@ impl Chatlist {
// timestamp
// - the list starts with the newest chats
//
// nb: the query currently shows messages from blocked
// contacts in groups. however, for normal-groups, this is
// okay as the message is also returned by dc_get_chat_msgs()
// (otherwise it would be hard to follow conversations, wa and
// tg do the same) for the deaddrop, however, they should
// really be hidden, however, _currently_ the deaddrop is not
// shown at all permanent in the chatlist.
// The query shows messages from blocked contacts in
// groups. Otherwise it would be hard to follow conversations.
let mut ids = if let Some(query_contact_id) = query_contact_id {
// show chats shared with a given contact
context.sql.query_map(
@@ -159,7 +143,7 @@ impl Chatlist {
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9
AND c.blocked=0
AND c.blocked!=1
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
@@ -186,7 +170,7 @@ impl Chatlist {
AND (hidden=0 OR state=?)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9
AND c.blocked=0
AND c.blocked!=1
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
@@ -220,7 +204,7 @@ impl Chatlist {
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND c.blocked!=1
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
@@ -232,14 +216,13 @@ impl Chatlist {
} else {
// show normal chatlist
let sort_id_up = if flag_for_forwarding {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.await
ChatId::lookup_by_contact(context, DC_CONTACT_ID_SELF)
.await?
.unwrap_or_default()
.0
} else {
ChatId::new(0)
};
let mut ids = context.sql.query_map(
let ids = context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -251,34 +234,25 @@ impl Chatlist {
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND NOT c.archived=?3
AND (c.blocked=0 OR (c.blocked=2 AND NOT ?3))
AND NOT c.archived=?4
GROUP BY c.id
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
process_row,
process_rows,
).await?;
if !flag_no_specials {
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
{
if !flag_for_forwarding {
ids.insert(
0,
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
);
}
}
add_archived_link_item = true;
}
ids
};
if add_archived_link_item && dc_get_archived_cnt(context).await > 0 {
if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 {
if ids.is_empty() && flag_add_alldone_hint {
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
}
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
ids.push((DC_CHAT_ID_ARCHIVED_LINK, None));
}
Ok(Chatlist { ids })
@@ -307,7 +281,7 @@ impl Chatlist {
/// Get a single message ID of a chatlist.
///
/// To get the message object from the message ID, use dc_get_msg().
pub fn get_msg_id(&self, index: usize) -> Result<MsgId> {
pub fn get_msg_id(&self, index: usize) -> Result<Option<MsgId>> {
match self.ids.get(index) {
Some((_chat_id, msg_id)) => Ok(*msg_id),
None => bail!("Chatlist index out of range"),
@@ -328,18 +302,19 @@ impl Chatlist {
/// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable.
/// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()).
// 0 if not applicable.
pub async fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot {
pub async fn get_summary(
&self,
context: &Context,
index: usize,
chat: Option<&Chat>,
) -> Result<Lot> {
// The summary is created by the chat, not by the last message.
// This is because we may want to display drafts here or stuff as
// "is typing".
// Also, sth. as "No messages" would not work if the summary comes from a message.
let (chat_id, lastmsg_id) = match self.ids.get(index) {
Some(ids) => ids,
None => {
let mut ret = Lot::new();
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return Lot::new();
}
None => bail!("Chatlist index out of range"),
};
Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
@@ -348,50 +323,50 @@ impl Chatlist {
pub async fn get_summary2(
context: &Context,
chat_id: ChatId,
lastmsg_id: MsgId,
lastmsg_id: Option<MsgId>,
chat: Option<&Chat>,
) -> Lot {
) -> Result<Lot> {
let mut ret = Lot::new();
let chat_loaded: Chat;
let chat = if let Some(chat) = chat {
chat
} else if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
} else {
let chat = Chat::load_from_db(context, chat_id).await?;
chat_loaded = chat;
&chat_loaded
} else {
return ret;
};
let (lastmsg, lastcontact) =
if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
if lastmsg.from_id == DC_CONTACT_ID_SELF {
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
let lastcontact =
Contact::load_from_db(context, lastmsg.from_id).await.ok();
(Some(lastmsg), lastcontact)
}
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
}
}
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
let lastmsg = Message::load_from_db(context, lastmsg_id).await?;
if lastmsg.from_id == DC_CONTACT_ID_SELF {
(Some(lastmsg), None)
} else {
(None, None)
};
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
let lastcontact =
Contact::load_from_db(context, lastmsg.from_id).await.ok();
(Some(lastmsg), lastcontact)
}
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
}
}
} else {
(None, None)
};
if chat.id.is_archived_link() {
ret.text2 = None;
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
} else if let Some(mut lastmsg) =
lastmsg.filter(|msg| msg.from_id != DC_CONTACT_ID_UNDEFINED)
{
ret.text2 = Some(stock_str::no_messages(context).await);
} else {
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context)
ret.fill(&mut lastmsg, chat, lastcontact.as_ref(), context)
.await;
} else {
ret.text2 = Some(stock_str::no_messages(context).await);
}
ret
Ok(ret)
}
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
@@ -400,46 +375,24 @@ impl Chatlist {
}
/// Returns the number of archived chats
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
context
pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
let count = context
.sql
.query_get_value(
context,
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
paramsv![],
.count(
"SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
paramsv![Blocked::Manually, ChatVisibility::Archived],
)
.await
.unwrap_or_default()
}
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
// We have an index over the state-column, this should be
// sufficient as there are typically only few fresh messages.
context
.sql
.query_get_value(
context,
concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN chats c",
" ON c.id=m.chat_id",
" WHERE m.state=10",
" AND m.hidden=0",
" AND c.blocked=2",
" ORDER BY m.timestamp DESC, m.id DESC;"
),
paramsv![],
)
.await
.await?;
Ok(count)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
@@ -466,7 +419,7 @@ mod tests {
// drafts are sorted to the top
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
chat_id2.set_draft(&t, Some(&mut msg)).await;
chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.get_chat_id(0), chat_id2);
@@ -545,6 +498,132 @@ mod tests {
assert_eq!(chats.len(), 1);
}
#[async_std::test]
async fn test_search_single_chat() -> anyhow::Result<()> {
let t = TestContext::new_alice().await;
// receive a one-to-one-message
dc_receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.com\n\
Subject: foo\n\
Message-ID: <msg1234@example.org>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
\n\
hello foo\n",
"INBOX",
1,
false,
)
.await?;
let chats = Chatlist::try_load(&t, 0, Some("Bob Authname"), None).await?;
// Contact request should be searchable
assert_eq!(chats.len(), 1);
let msg = t.get_last_msg().await;
let chat_id = msg.get_chat_id();
chat_id.accept(&t).await.unwrap();
let contacts = get_chat_contacts(&t, chat_id).await?;
let contact_id = *contacts.first().unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Authname");
// check, the one-to-one-chat can be found using chatlist search query
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
// change the name of the contact; this also changes the name of the one-to-one-chat
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Nickname");
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
assert_eq!(chats.len(), 0);
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
assert_eq!(chats.len(), 1);
// revert contact to authname, this again changes the name of the one-to-one-chat
let test_id = Contact::create(&t, "", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Authname");
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
assert_eq!(chats.len(), 0);
Ok(())
}
#[async_std::test]
async fn test_search_single_chat_without_authname() -> anyhow::Result<()> {
let t = TestContext::new_alice().await;
// receive a one-to-one-message without authname set
dc_receive_imf(
&t,
b"From: bob@example.org\n\
To: alice@example.com\n\
Subject: foo\n\
Message-ID: <msg5678@example.org>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2021 22:38:57 +0000\n\
\n\
hello foo\n",
"INBOX",
1,
false,
)
.await?;
let msg = t.get_last_msg().await;
let chat_id = msg.get_chat_id();
chat_id.accept(&t).await.unwrap();
let contacts = get_chat_contacts(&t, chat_id).await?;
let contact_id = *contacts.first().unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "bob@example.org");
// check, the one-to-one-chat can be found using chatlist search query
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
// change the name of the contact; this also changes the name of the one-to-one-chat
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Nickname");
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
assert_eq!(chats.len(), 0); // email-addresses are searchable in contacts, not in chats
let chats = Chatlist::try_load(&t, 0, Some("Bob Nickname"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
// revert name change, this again changes the name of the one-to-one-chat to the email-address
let test_id = Contact::create(&t, "", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "bob@example.org");
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
assert_eq!(chats.len(), 0);
// finally, also check that a simple substring-search is working with email-addresses
let chats = Chatlist::try_load(&t, 0, Some("b@exa"), None).await?;
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t, 0, Some("b@exac"), None).await?;
assert_eq!(chats.len(), 0);
Ok(())
}
#[async_std::test]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
@@ -554,10 +633,10 @@ mod tests {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
chat_id1.set_draft(&t, Some(&mut msg)).await;
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
let summary = chats.get_summary(&t, 0, None).await;
let summary = chats.get_summary(&t, 0, None).await.unwrap();
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
}
}

View File

@@ -8,8 +8,8 @@ use hsluv::hsluv_to_rgb;
use sha1::{Digest, Sha1};
/// Converts an identifier to Hue angle.
fn str_to_angle(s: impl AsRef<str>) -> f64 {
let bytes = s.as_ref().as_bytes();
fn str_to_angle(s: &str) -> f64 {
let bytes = s.as_bytes();
let result = Sha1::digest(bytes);
let checksum: u16 = result.get(0).map_or(0, |&x| u16::from(x))
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
@@ -31,7 +31,7 @@ fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 {
///
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
/// half (50.0) to make colors suitable both for light and dark theme.
pub(crate) fn str_to_color(s: impl AsRef<str>) -> u32 {
pub(crate) fn str_to_color(s: &str) -> u32 {
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
}
@@ -42,7 +42,7 @@ mod tests {
#[test]
fn test_str_to_angle() {
// Test against test vectors from
// https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd
// <https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd>
assert!((str_to_angle("Romeo") - 327.255249).abs() < 1e-6);
assert!((str_to_angle("juliet@capulet.lit") - 209.410400).abs() < 1e-6);
assert!((str_to_angle("😺") - 331.199341).abs() < 1e-6);

View File

@@ -1,5 +1,6 @@
//! # Key-value configuration management
use anyhow::Result;
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
@@ -17,7 +18,18 @@ use crate::stock_str;
/// The available configuration keys.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, AsRefStr, EnumIter, EnumProperty,
Debug,
Clone,
Copy,
PartialEq,
Eq,
Display,
EnumString,
AsRefStr,
EnumIter,
EnumProperty,
PartialOrd,
Ord,
)]
#[strum(serialize_all = "snake_case")]
pub enum Config {
@@ -150,69 +162,66 @@ pub enum Config {
}
impl Context {
pub async fn config_exists(&self, key: Config) -> bool {
self.sql.get_raw_config(self, key).await.is_some()
pub async fn config_exists(&self, key: Config) -> Result<bool> {
Ok(self.sql.get_raw_config(key).await?.is_some())
}
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub async fn get_config(&self, key: Config) -> Option<String> {
pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(self, key).await;
let rel_path = self.sql.get_raw_config(key).await?;
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(self, key).await,
_ => self.sql.get_raw_config(key).await?,
};
if value.is_some() {
return value;
return Ok(value);
}
// Default values
match key {
Config::Selfstatus => Some(stock_str::status_line(self).await),
Config::ConfiguredInboxFolder => Some("INBOX".to_owned()),
_ => key.get_str("default").map(|s| s.to_string()),
Config::Selfstatus => Ok(Some(stock_str::status_line(self).await)),
Config::ConfiguredInboxFolder => Ok(Some("INBOX".to_owned())),
_ => Ok(key.get_str("default").map(|s| s.to_string())),
}
}
pub async fn get_config_int(&self, key: Config) -> i32 {
pub async fn get_config_int(&self, key: Config) -> Result<i32> {
self.get_config(key)
.await
.and_then(|s| s.parse().ok())
.unwrap_or_default()
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_i64(&self, key: Config) -> i64 {
pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
self.get_config(key)
.await
.and_then(|s| s.parse().ok())
.unwrap_or_default()
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_u64(&self, key: Config) -> u64 {
pub async fn get_config_u64(&self, key: Config) -> Result<u64> {
self.get_config(key)
.await
.and_then(|s| s.parse().ok())
.unwrap_or_default()
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_bool(&self, key: Config) -> bool {
self.get_config_int(key).await != 0
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
Ok(self.get_config_int(key).await? != 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 async fn get_config_delete_server_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteServerAfter).await {
0 => None,
1 => Some(0),
x => Some(x as i64),
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteServerAfter).await? {
0 => Ok(None),
1 => Ok(Some(0)),
x => Ok(Some(x as i64)),
}
}
@@ -220,41 +229,46 @@ impl Context {
///
/// The provider is determined by `get_provider_info()` during configuration and then saved
/// to the db in `param.save_to_database()`, together with all the other `configured_*` values.
pub async fn get_configured_provider(&self) -> Option<&'static Provider> {
get_provider_by_id(&self.get_config(Config::ConfiguredProvider).await?)
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? {
return Ok(get_provider_by_id(&cfg));
}
Ok(None)
}
/// Gets configured "delete_device_after" value.
///
/// `None` means never delete the message, `Some(x)` means delete
/// after `x` seconds.
pub async fn get_config_delete_device_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteDeviceAfter).await {
0 => None,
x => Some(x as i64),
pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteDeviceAfter).await? {
0 => Ok(None),
x => Ok(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 async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
match key {
Config::Selfavatar => {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
.await?;
self.sql
.set_raw_config_bool(self, "attach_selfavatar", true)
.set_raw_config_bool("attach_selfavatar", true)
.await?;
match value {
Some(value) => {
let blob = BlobObject::new_from_path(self, value).await?;
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
blob.recode_to_avatar_size(self).await?;
self.sql
.set_raw_config(self, key, Some(blob.as_name()))
.await
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
Ok(())
}
None => {
self.sql.set_raw_config(key, None).await?;
Ok(())
}
None => self.sql.set_raw_config(self, key, None).await,
}
}
Config::Selfstatus => {
@@ -265,10 +279,15 @@ impl Context {
value
};
self.sql.set_raw_config(self, key, val).await
self.sql.set_raw_config(key, val).await?;
Ok(())
}
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(self, key, value).await;
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
// Force chatlist reload to delete old messages immediately.
self.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
@@ -278,20 +297,29 @@ impl Context {
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql.set_raw_config(self, key, value.as_deref()).await
self.sql.set_raw_config(key, value.as_deref()).await?;
Ok(())
}
Config::DeleteServerAfter => {
let ret = self.sql.set_raw_config(self, key, value).await;
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
job::schedule_resync(self).await;
ret
}
_ => self.sql.set_raw_config(self, key, value).await,
_ => {
self.sql.set_raw_config(key, value).await?;
Ok(())
}
}
}
pub async fn set_config_bool(&self, key: Config, value: bool) -> crate::sql::Result<()> {
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
self.set_config(key, if value { Some("1") } else { None })
.await
.await?;
Ok(())
}
}
@@ -314,12 +342,8 @@ mod tests {
use std::string::ToString;
use crate::constants;
use crate::constants::BALANCED_AVATAR_SIZE;
use crate::test_utils::TestContext;
use image::GenericImageView;
use num_traits::FromPrimitive;
use std::fs::File;
use std::io::Write;
#[test]
fn test_to_string() {
@@ -333,93 +357,17 @@ mod tests {
);
}
#[async_std::test]
async fn test_selfavatar_outside_blobdir() {
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.jpg");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
File::create(&avatar_src)
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists().await);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists().await);
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
let avatar_cfg = t.get_config(Config::Selfavatar).await;
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap();
assert_eq!(img.width(), 1000);
assert_eq!(img.height(), 1000);
let img = image::open(avatar_blob).unwrap();
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
}
#[async_std::test]
async fn test_selfavatar_in_blobdir() {
let t = TestContext::new().await;
let avatar_src = t.get_blobdir().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
File::create(&avatar_src)
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let img = image::open(&avatar_src).unwrap();
assert_eq!(img.width(), 900);
assert_eq!(img.height(), 900);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
let avatar_cfg = t.get_config(Config::Selfavatar).await;
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap();
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
}
#[async_std::test]
async fn test_selfavatar_copy_without_recode() {
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
File::create(&avatar_src)
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.png");
assert!(!avatar_blob.exists().await);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists().await);
assert_eq!(
std::fs::metadata(&avatar_blob).unwrap().len(),
avatar_bytes.len() as u64
);
let avatar_cfg = t.get_config(Config::Selfavatar).await;
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
}
#[async_std::test]
async fn test_media_quality_config_option() {
let t = TestContext::new().await;
let media_quality = t.get_config_int(Config::MediaQuality).await;
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
assert_eq!(media_quality, 0);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Balanced);
t.set_config(Config::MediaQuality, Some("1")).await.unwrap();
let media_quality = t.get_config_int(Config::MediaQuality).await;
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
assert_eq!(media_quality, 1);
assert_eq!(constants::MediaQuality::Worse as i32, 1);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();

View File

@@ -50,8 +50,11 @@ macro_rules! progress {
impl Context {
/// Checks if the context is already configured.
pub async fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool(self, "configured").await
pub async fn is_configured(&self) -> Result<bool> {
self.sql
.get_raw_config_bool("configured")
.await
.map_err(Into::into)
}
/// Configures this account with the currently set parameters.
@@ -84,14 +87,14 @@ impl Context {
async fn inner_configure(&self) -> Result<()> {
info!(self, "Configure ...");
let mut param = LoginParam::from_database(self, "").await;
let mut param = LoginParam::from_database(self, "").await?;
let success = configure(self, &mut param).await;
self.set_config(Config::NotifyAboutWrongPw, None).await?;
if let Some(provider) = param.provider {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
if !self.config_exists(def.key).await {
if !self.config_exists(def.key).await? {
info!(self, "apply config_defaults {}={}", def.key, def.value);
self.set_config(def.key, Some(def.value)).await?;
} else {
@@ -177,13 +180,13 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, &param.addr, &param.imap.password)
.await
.await?
.and_then(|e| e.parse().ok())
{
info!(ctx, "Authorized address is {}", oauth2_addr);
param.addr = oauth2_addr;
ctx.sql
.set_raw_config(ctx, "addr", Some(param.addr.as_str()))
.set_raw_config("addr", Some(param.addr.as_str()))
.await?;
}
progress!(ctx, 20);
@@ -217,7 +220,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
if let Some(provider) = provider::get_provider_info(&param_domain).await {
param.provider = Some(provider);
match provider.status {
provider::Status::OK | provider::Status::PREPARATION => {
provider::Status::Ok | provider::Status::Preparation => {
if provider.server.is_empty() {
info!(ctx, "offline autoconfig found, but no servers defined");
param_autoconfig = None;
@@ -232,8 +235,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::EMAIL => param.addr.to_string(),
UsernamePattern::EMAILLOCALPART => {
UsernamePattern::Email => param.addr.to_string(),
UsernamePattern::Emaillocalpart => {
if let Some(at) = param.addr.find('@') {
param.addr.split_at(at).0.to_string()
} else {
@@ -247,7 +250,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
param_autoconfig = Some(servers)
}
}
provider::Status::BROKEN => {
provider::Status::Broken => {
info!(ctx, "offline autoconfig found, provider is broken");
param_autoconfig = None;
}
@@ -266,10 +269,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let mut servers = param_autoconfig.unwrap_or_default();
if !servers
.iter()
.any(|server| server.protocol == Protocol::IMAP)
.any(|server| server.protocol == Protocol::Imap)
{
servers.push(ServerParams {
protocol: Protocol::IMAP,
protocol: Protocol::Imap,
hostname: param.imap.server.clone(),
port: param.imap.port,
socket: param.imap.security,
@@ -278,10 +281,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
if !servers
.iter()
.any(|server| server.protocol == Protocol::SMTP)
.any(|server| server.protocol == Protocol::Smtp)
{
servers.push(ServerParams {
protocol: Protocol::SMTP,
protocol: Protocol::Smtp,
hostname: param.smtp.server.clone(),
port: param.smtp.port,
socket: param.smtp.security,
@@ -300,7 +303,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let smtp_addr = param.addr.clone();
let smtp_servers: Vec<ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::SMTP)
.filter(|params| params.protocol == Protocol::Smtp)
.cloned()
.collect();
let provider_strict_tls = param.provider.map_or(false, |provider| provider.strict_tls);
@@ -342,13 +345,11 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 600);
// Configure IMAP
let (_s, r) = async_std::channel::bounded(1);
let mut imap = Imap::new(r);
let mut imap_configured = false;
let mut imap: Option<Imap> = None;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::IMAP)
.filter(|params| params.protocol == Protocol::Imap)
.collect();
let imap_servers_count = imap_servers.len();
let mut errors = Vec::new();
@@ -358,18 +359,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
param.imap.port = imap_server.port;
param.imap.security = imap_server.socket;
match try_imap_one_param(
ctx,
&param.imap,
&param.addr,
oauth2,
provider_strict_tls,
&mut imap,
)
.await
{
Ok(_) => {
imap_configured = true;
match try_imap_one_param(ctx, &param.imap, &param.addr, oauth2, provider_strict_tls).await {
Ok(configured_imap) => {
imap = Some(configured_imap);
break;
}
Err(e) => errors.push(e),
@@ -379,9 +371,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
if !imap_configured {
bail!(nicer_configuration_error(ctx, errors).await);
}
let mut imap = match imap {
Some(imap) => imap,
None => bail!(nicer_configuration_error(ctx, errors).await),
};
progress!(ctx, 850);
@@ -397,8 +390,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 900);
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
|| ctx.get_config_bool(Config::MvboxMove).await;
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await?
|| ctx.get_config_bool(Config::MvboxMove).await?;
imap.configure_folders(ctx, create_mvbox).await?;
@@ -413,7 +406,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// "configured_" prefix; also write the "configured"-flag */
// the trailing underscore is correct
param.save_to_database(ctx, "configured_").await?;
ctx.sql.set_raw_config_bool(ctx, "configured", true).await?;
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;
@@ -446,7 +439,7 @@ async fn get_autoconfig(
) -> Option<Vec<ServerParams>> {
if let Ok(res) = moz_autoconfigure(
ctx,
format!(
&format!(
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
param_domain, param_addr_urlencoded
),
@@ -460,8 +453,8 @@ async fn get_autoconfig(
if let Ok(res) = moz_autoconfigure(
ctx,
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
format!(
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense
&format!(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
&param_domain, &param_addr_urlencoded
),
@@ -500,7 +493,7 @@ async fn get_autoconfig(
// always SSL for Thunderbird's database
if let Ok(res) = moz_autoconfigure(
ctx,
format!("https://autoconfig.thunderbird.net/v1.1/{}", &param_domain),
&format!("https://autoconfig.thunderbird.net/v1.1/{}", &param_domain),
param,
)
.await
@@ -517,26 +510,38 @@ async fn try_imap_one_param(
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
imap: &mut Imap,
) -> Result<(), ConfigurationError> {
) -> Result<Imap, ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
if let Err(err) = imap
.connect(context, param, addr, oauth2, provider_strict_tls)
.await
{
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
} else {
info!(context, "success: {}", inf);
Ok(())
let (_s, r) = async_std::channel::bounded(1);
let mut imap = match Imap::new(param, addr, oauth2, provider_strict_tls, r).await {
Err(err) => {
info!(context, "failure: {}", err);
return Err(ConfigurationError {
config: inf,
msg: err.to_string(),
});
}
Ok(imap) => imap,
};
match imap.connect(context).await {
Err(err) => {
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
}
Ok(()) => {
info!(context, "success: {}", inf);
Ok(imap)
}
}
}
@@ -613,10 +618,10 @@ pub enum Error {
},
#[error("Failed to get URL: {0}")]
ReadUrlError(#[from] self::read_url::Error),
ReadUrl(#[from] self::read_url::Error),
#[error("Number of redirection is exceeded")]
RedirectionError,
Redirection,
}
#[cfg(test)]

View File

@@ -1,6 +1,6 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration
//! Documentation: <https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
use quick_xml::events::{BytesStart, Event};
use std::io::BufRead;
@@ -115,8 +115,8 @@ fn parse_server<B: BufRead>(
MozConfigTag::Username => username = Some(val),
MozConfigTag::Sockettype => {
sockettype = match val.to_lowercase().as_ref() {
"ssl" => Socket::SSL,
"starttls" => Socket::STARTTLS,
"ssl" => Socket::Ssl,
"starttls" => Socket::Starttls,
"plain" => Socket::Plain,
_ => Socket::Automatic,
}
@@ -233,8 +233,8 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
.chain(moz_ac.outgoing_servers.into_iter())
.filter_map(|server| {
let protocol = match server.typ.as_ref() {
"imap" => Some(Protocol::IMAP),
"smtp" => Some(Protocol::SMTP),
"imap" => Some(Protocol::Imap),
"smtp" => Some(Protocol::Smtp),
_ => None,
};
Some(ServerParams {
@@ -251,10 +251,10 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
pub(crate) async fn moz_autoconfigure(
context: &Context,
url: impl AsRef<str>,
url: &str,
param_in: &LoginParam,
) -> Result<Vec<ServerParams>, Error> {
let xml_raw = read_url(context, url.as_ref()).await?;
let xml_raw = read_url(context, url).await?;
let res = parse_serverparams(&param_in.addr, &xml_raw);
if let Err(err) = &res {
@@ -276,10 +276,10 @@ mod tests {
fn test_parse_outlook_autoconfig() {
let xml_raw = include_str!("../../test-data/autoconfig/outlook.com.xml");
let res = parse_serverparams("example@outlook.com", xml_raw).expect("XML parsing failed");
assert_eq!(res[0].protocol, Protocol::IMAP);
assert_eq!(res[0].protocol, Protocol::Imap);
assert_eq!(res[0].hostname, "outlook.office365.com");
assert_eq!(res[0].port, 993);
assert_eq!(res[1].protocol, Protocol::SMTP);
assert_eq!(res[1].protocol, Protocol::Smtp);
assert_eq!(res[1].hostname, "smtp.office365.com");
assert_eq!(res[1].port, 587);
}
@@ -295,25 +295,25 @@ mod tests {
assert_eq!(res.incoming_servers[0].typ, "imap");
assert_eq!(res.incoming_servers[0].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[0].port, 993);
assert_eq!(res.incoming_servers[0].sockettype, Socket::SSL);
assert_eq!(res.incoming_servers[0].sockettype, Socket::Ssl);
assert_eq!(res.incoming_servers[0].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[1].typ, "imap");
assert_eq!(res.incoming_servers[1].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[1].port, 143);
assert_eq!(res.incoming_servers[1].sockettype, Socket::STARTTLS);
assert_eq!(res.incoming_servers[1].sockettype, Socket::Starttls);
assert_eq!(res.incoming_servers[1].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[2].typ, "pop3");
assert_eq!(res.incoming_servers[2].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[2].port, 995);
assert_eq!(res.incoming_servers[2].sockettype, Socket::SSL);
assert_eq!(res.incoming_servers[2].sockettype, Socket::Ssl);
assert_eq!(res.incoming_servers[2].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[3].typ, "pop3");
assert_eq!(res.incoming_servers[3].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[3].port, 110);
assert_eq!(res.incoming_servers[3].sockettype, Socket::STARTTLS);
assert_eq!(res.incoming_servers[3].sockettype, Socket::Starttls);
assert_eq!(res.incoming_servers[3].username, "example@lakenet.ch");
assert_eq!(res.outgoing_servers.len(), 1);
@@ -321,7 +321,7 @@ mod tests {
assert_eq!(res.outgoing_servers[0].typ, "smtp");
assert_eq!(res.outgoing_servers[0].hostname, "mail.lakenet.ch");
assert_eq!(res.outgoing_servers[0].port, 587);
assert_eq!(res.outgoing_servers[0].sockettype, Socket::STARTTLS);
assert_eq!(res.outgoing_servers[0].sockettype, Socket::Starttls);
assert_eq!(res.outgoing_servers[0].username, "example@lakenet.ch");
}
}

View File

@@ -15,27 +15,27 @@ use super::{Error, ServerParams};
/// Result of parsing a single `Protocol` tag.
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox>
#[derive(Debug)]
struct ProtocolTag {
/// Server type, such as "IMAP", "SMTP" or "POP3".
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox>
pub typ: String,
/// Server identifier, hostname or IP address for IMAP and SMTP.
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox>
pub server: String,
/// Network port.
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox>
pub port: u16,
/// Whether connection should be secure, "on" or "off", default is "on".
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox>
pub ssl: bool,
}
@@ -169,8 +169,8 @@ fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
.filter_map(|protocol| {
Some(ServerParams {
protocol: match protocol.typ.to_lowercase().as_ref() {
"imap" => Some(Protocol::IMAP),
"smtp" => Some(Protocol::SMTP),
"imap" => Some(Protocol::Imap),
"smtp" => Some(Protocol::Smtp),
_ => None,
}?,
socket: match protocol.ssl {
@@ -203,7 +203,7 @@ pub(crate) async fn outlk_autodiscover(
}
}
}
Err(Error::RedirectionError)
Err(Error::Redirection)
}
#[cfg(test)]

View File

@@ -25,16 +25,20 @@ pub(crate) struct ServerParams {
}
impl ServerParams {
pub(crate) fn expand_usernames(mut self, addr: &str) -> Vec<ServerParams> {
fn expand_usernames(self, addr: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
if self.username.is_empty() {
self.username = addr.to_string();
res.push(self.clone());
res.push(Self {
username: addr.to_string(),
..self.clone()
});
if let Some(at) = addr.find('@') {
self.username = addr.split_at(at).0.to_string();
res.push(self);
res.push(Self {
username: addr.split_at(at).0.to_string(),
..self
});
}
} else {
res.push(self)
@@ -42,75 +46,87 @@ impl ServerParams {
res
}
pub(crate) fn expand_hostnames(mut self, param_domain: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
fn expand_hostnames(self, param_domain: &str) -> Vec<ServerParams> {
if self.hostname.is_empty() {
self.hostname = param_domain.to_string();
res.push(self.clone());
self.hostname = match self.protocol {
Protocol::IMAP => "imap.".to_string() + param_domain,
Protocol::SMTP => "smtp.".to_string() + param_domain,
};
res.push(self.clone());
self.hostname = "mail.".to_string() + param_domain;
res.push(self);
vec![
Self {
hostname: param_domain.to_string(),
..self.clone()
},
Self {
hostname: match self.protocol {
Protocol::Imap => "imap.".to_string() + param_domain,
Protocol::Smtp => "smtp.".to_string() + param_domain,
},
..self.clone()
},
Self {
hostname: "mail.".to_string() + param_domain,
..self
},
]
} else {
res.push(self);
vec![self]
}
res
}
pub(crate) fn expand_ports(mut self) -> Vec<ServerParams> {
fn expand_ports(mut self) -> Vec<ServerParams> {
// Try to infer port from socket security.
if self.port == 0 {
self.port = match self.socket {
Socket::SSL => match self.protocol {
Protocol::IMAP => 993,
Protocol::SMTP => 465,
Socket::Ssl => match self.protocol {
Protocol::Imap => 993,
Protocol::Smtp => 465,
},
Socket::STARTTLS | Socket::Plain => match self.protocol {
Protocol::IMAP => 143,
Protocol::SMTP => 587,
Socket::Starttls | Socket::Plain => match self.protocol {
Protocol::Imap => 143,
Protocol::Smtp => 587,
},
Socket::Automatic => 0,
}
}
let mut res = Vec::new();
if self.port == 0 {
// Neither port nor security is set.
//
// Try common secure combinations.
// Try STARTTLS
self.socket = Socket::STARTTLS;
self.port = match self.protocol {
Protocol::IMAP => 143,
Protocol::SMTP => 587,
};
res.push(self.clone());
// Try TLS
self.socket = Socket::SSL;
self.port = match self.protocol {
Protocol::IMAP => 993,
Protocol::SMTP => 465,
};
res.push(self);
vec![
// Try STARTTLS
Self {
socket: Socket::Starttls,
port: match self.protocol {
Protocol::Imap => 143,
Protocol::Smtp => 587,
},
..self.clone()
},
// Try TLS
Self {
socket: Socket::Ssl,
port: match self.protocol {
Protocol::Imap => 993,
Protocol::Smtp => 465,
},
..self
},
]
} else if self.socket == Socket::Automatic {
// Try TLS over user-provided port.
self.socket = Socket::SSL;
res.push(self.clone());
// Try STARTTLS over user-provided port.
self.socket = Socket::STARTTLS;
res.push(self);
vec![
// Try TLS over user-provided port.
Self {
socket: Socket::Ssl,
..self.clone()
},
// Try STARTTLS over user-provided port.
Self {
socket: Socket::Starttls,
..self
},
]
} else {
res.push(self);
vec![self]
}
res
}
}
@@ -140,10 +156,10 @@ mod tests {
fn test_expand_param_vector() {
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::IMAP,
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 0,
socket: Socket::SSL,
socket: Socket::Ssl,
username: "foobar".to_string(),
}],
"foobar@example.net",
@@ -153,12 +169,44 @@ mod tests {
assert_eq!(
v,
vec![ServerParams {
protocol: Protocol::IMAP,
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 993,
socket: Socket::SSL,
socket: Socket::Ssl,
username: "foobar".to_string(),
}],
);
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Automatic,
username: "foobar".to_string(),
}],
"foobar@example.net",
"example.net",
);
assert_eq!(
v,
vec![
ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Ssl,
username: "foobar".to_string()
},
ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Starttls,
username: "foobar".to_string()
}
],
);
}
}

View File

@@ -3,6 +3,8 @@ use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use crate::chat::ChatId;
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
#[derive(
@@ -19,11 +21,11 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
Serialize,
Deserialize,
)]
#[repr(u8)]
#[repr(i8)]
pub enum Blocked {
Not = 0,
Manually = 1,
Deaddrop = 2,
Request = 2,
}
impl Default for Blocked {
@@ -121,16 +123,14 @@ pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
// do not use too small value that will annoy users checking for nonexistant updates.
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub const DC_CHAT_ID_DEADDROP: u32 = 1;
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
pub const DC_CHAT_ID_TRASH: u32 = 3;
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ARCHIVED_LINK: u32 = 6;
pub const DC_CHAT_ID_ARCHIVED_LINK: ChatId = ChatId::new(6);
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ALLDONE_HINT: u32 = 7;
pub const DC_CHAT_ID_ALLDONE_HINT: ChatId = ChatId::new(7);
/// larger chat IDs are "real" chats, their messages are "real" messages.
pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
#[derive(
Debug,
@@ -171,7 +171,7 @@ pub const DC_ELLIPSE: &str = "[...]";
/// to keep bubbles and chat flow usable,
/// and to avoid problems with controls using very long texts,
/// we limit the text length to DC_DESIRED_TEXT_LEN.
/// if the text is longer, the full text can be retrieved usind has_html()/get_html().
/// if the text is longer, the full text can be retrieved using has_html()/get_html().
///
/// we are using a bit less than DC_MAX_GET_TEXT_LEN to avoid cutting twice
/// (a bit less as truncation may not be exact and ellipses may be added).
@@ -261,7 +261,7 @@ pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
Serialize,
Deserialize,
)]
#[repr(i32)]
#[repr(u32)]
pub enum Viewtype {
Unknown = 0,
@@ -320,16 +320,6 @@ impl Default for Viewtype {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_display_works_as_expected() {
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
}
}
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
@@ -338,3 +328,100 @@ pub enum KeyType {
Public = 0,
Private = 1,
}
#[cfg(test)]
mod tests {
use super::*;
use num_traits::FromPrimitive;
#[test]
fn test_derive_display_works_as_expected() {
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
}
#[test]
fn test_viewtype_values() {
// values may be written to disk and must not change
assert_eq!(Viewtype::Unknown, Viewtype::default());
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
assert_eq!(
Viewtype::VideochatInvitation,
Viewtype::from_i32(70).unwrap()
);
}
#[test]
fn test_chattype_values() {
// values may be written to disk and must not change
assert_eq!(Chattype::Undefined, Chattype::default());
assert_eq!(Chattype::Undefined, Chattype::from_i32(0).unwrap());
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
}
#[test]
fn test_keygentype_values() {
// values may be written to disk and must not change
assert_eq!(KeyGenType::Default, KeyGenType::default());
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
}
#[test]
fn test_keytype_values() {
// values may be written to disk and must not change
assert_eq!(KeyType::Public, KeyType::from_i32(0).unwrap());
assert_eq!(KeyType::Private, KeyType::from_i32(1).unwrap());
}
#[test]
fn test_showemails_values() {
// values may be written to disk and must not change
assert_eq!(ShowEmails::Off, ShowEmails::default());
assert_eq!(ShowEmails::Off, ShowEmails::from_i32(0).unwrap());
assert_eq!(
ShowEmails::AcceptedContacts,
ShowEmails::from_i32(1).unwrap()
);
assert_eq!(ShowEmails::All, ShowEmails::from_i32(2).unwrap());
}
#[test]
fn test_blocked_values() {
// values may be written to disk and must not change
assert_eq!(Blocked::Not, Blocked::default());
assert_eq!(Blocked::Not, Blocked::from_i32(0).unwrap());
assert_eq!(Blocked::Manually, Blocked::from_i32(1).unwrap());
assert_eq!(Blocked::Request, Blocked::from_i32(2).unwrap());
}
#[test]
fn test_mediaquality_values() {
// values may be written to disk and must not change
assert_eq!(MediaQuality::Balanced, MediaQuality::default());
assert_eq!(MediaQuality::Balanced, MediaQuality::from_i32(0).unwrap());
assert_eq!(MediaQuality::Worse, MediaQuality::from_i32(1).unwrap());
}
#[test]
fn test_videochattype_values() {
// values may be written to disk and must not change
assert_eq!(VideochatType::Unknown, VideochatType::default());
assert_eq!(VideochatType::Unknown, VideochatType::from_i32(0).unwrap());
assert_eq!(
VideochatType::BasicWebrtc,
VideochatType::from_i32(1).unwrap()
);
assert_eq!(VideochatType::Jitsi, VideochatType::from_i32(2).unwrap());
}
}

View File

@@ -1,6 +1,8 @@
//! Contacts module
use anyhow::{bail, ensure, format_err, Context as _, Result};
use std::convert::{TryFrom, TryInto};
use anyhow::{bail, ensure, format_err, Result};
use async_std::path::PathBuf;
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
@@ -12,8 +14,8 @@ use crate::chat::ChatId;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, DC_CHAT_ID_DEADDROP, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE_ADDR,
DC_CONTACT_ID_LAST_SPECIAL, DC_CONTACT_ID_SELF, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY,
Blocked, Chattype, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE_ADDR, DC_CONTACT_ID_LAST_SPECIAL,
DC_CONTACT_ID_SELF, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY,
};
use crate::context::Context;
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, EmailAddress};
@@ -79,7 +81,7 @@ pub struct Contact {
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(i32)]
#[repr(u32)]
pub enum Origin {
Unknown = 0,
@@ -173,8 +175,8 @@ pub enum VerifiedStatus {
}
impl Contact {
pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
let mut res = context
pub async fn load_from_db(context: &Context, contact_id: u32) -> Result<Self> {
let mut contact = context
.sql
.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
@@ -182,35 +184,43 @@ impl Contact {
WHERE c.id=?;",
paramsv![contact_id as i32],
|row| {
let name: String = row.get(0)?;
let addr: String = row.get(1)?;
let origin: Origin = row.get(2)?;
let blocked: Option<bool> = row.get(3)?;
let authname: String = row.get(4)?;
let param: String = row.get(5)?;
let status: Option<String> = row.get(6)?;
let contact = Self {
id: contact_id,
name: row.get::<_, String>(0)?,
authname: row.get::<_, String>(4)?,
addr: row.get::<_, String>(1)?,
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
origin: row.get(2)?,
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
status: row.get(6).unwrap_or_default(),
name,
authname,
addr,
blocked: blocked.unwrap_or_default(),
origin,
param: param.parse().unwrap_or_default(),
status: status.unwrap_or_default(),
};
Ok(contact)
},
)
.await?;
if contact_id == DC_CONTACT_ID_SELF {
res.name = stock_str::self_msg(context).await;
res.addr = context
contact.name = stock_str::self_msg(context).await;
contact.addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
res.status = context
contact.status = context
.get_config(Config::Selfstatus)
.await
.await?
.unwrap_or_default();
} else if contact_id == DC_CONTACT_ID_DEVICE {
res.name = stock_str::device_messages(context).await;
res.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
contact.name = stock_str::device_messages(context).await;
contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
contact.status = stock_str::device_messages_hint(context).await;
}
Ok(res)
Ok(contact)
}
/// Returns `true` if this contact is blocked.
@@ -227,13 +237,13 @@ impl Contact {
}
/// Block the given contact.
pub async fn block(context: &Context, id: u32) {
set_block_contact(context, id, true).await;
pub async fn block(context: &Context, id: u32) -> Result<()> {
set_block_contact(context, id, true).await
}
/// Unblock the given contact.
pub async fn unblock(context: &Context, id: u32) {
set_block_contact(context, id, false).await;
pub async fn unblock(context: &Context, id: u32) -> Result<()> {
set_block_contact(context, id, false).await
}
/// Add a single contact as a result of an _explicit_ user action.
@@ -245,21 +255,14 @@ impl Contact {
/// a bunch of addresses.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn create(
context: &Context,
name: impl AsRef<str>,
addr: impl AsRef<str>,
) -> Result<u32> {
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<u32> {
let name = improve_single_line_input(name);
ensure!(
!addr.as_ref().is_empty(),
"Cannot create contact with empty address"
);
ensure!(!addr.is_empty(), "Cannot create contact with empty address");
let (name, addr) = sanitize_name_and_addr(name, addr);
let (name, addr) = sanitize_name_and_addr(&name, addr);
let (contact_id, sth_modified) =
Contact::add_or_lookup(context, name, addr, Origin::ManuallyCreated).await?;
Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated).await?;
let blocked = Contact::is_blocked_load(context, contact_id).await;
match sth_modified {
Modifier::None => {}
@@ -268,27 +271,22 @@ impl Contact {
}
}
if blocked {
Contact::unblock(context, contact_id).await;
Contact::unblock(context, contact_id).await?;
}
Ok(contact_id)
}
/// Mark messages from a contact as noticed.
/// The contact is expected to belong to the deaddrop,
/// therefore, DC_EVENT_MSGS_NOTICED(DC_CHAT_ID_DEADDROP) is emitted.
pub async fn mark_noticed(context: &Context, id: u32) {
if context
pub async fn mark_noticed(context: &Context, id: u32) -> Result<()> {
context
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
)
.await
.is_ok()
{
context.emit_event(EventType::MsgsNoticed(ChatId::new(DC_CHAT_ID_DEADDROP)));
}
.await?;
Ok(())
}
/// Check if an e-mail address belongs to a known and unblocked contact.
@@ -308,21 +306,25 @@ impl Contact {
let addr_normalized = addr_normalize(addr.as_ref());
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await {
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await? {
if addr_cmp(addr_normalized, addr_self) {
return Ok(Some(DC_CONTACT_ID_SELF));
}
}
context.sql.query_get_value_result(
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
paramsv![
addr_normalized,
DC_CONTACT_ID_LAST_SPECIAL as i32,
min_origin as u32,
],
)
.await
.context("lookup_id_by_addr: SQL query failed")
let id = context
.sql
.query_get_value(
"SELECT id FROM contacts \
WHERE addr=?1 COLLATE NOCASE \
AND id>?2 AND origin>=?3 AND blocked=0;",
paramsv![
addr_normalized,
DC_CONTACT_ID_LAST_SPECIAL as i32,
min_origin as u32,
],
)
.await?;
Ok(id)
}
/// Lookup a contact and create it if it does not exist yet.
@@ -352,22 +354,19 @@ impl Contact {
/// Returns the contact_id and a `Modifier` value indicating if a modification occured.
pub(crate) async fn add_or_lookup(
context: &Context,
name: impl AsRef<str>,
addr: impl AsRef<str>,
name: &str,
addr: &str,
mut origin: Origin,
) -> Result<(u32, Modifier)> {
let mut sth_modified = Modifier::None;
ensure!(
!addr.as_ref().is_empty(),
"Can not add_or_lookup empty address"
);
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
ensure!(origin != Origin::Unknown, "Missing valid origin");
let addr = addr_normalize(addr.as_ref()).to_string();
let addr = addr_normalize(addr).to_string();
let addr_self = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
if addr_cmp(&addr, addr_self) {
@@ -379,16 +378,12 @@ impl Contact {
context,
"Bad address \"{}\" for contact \"{}\".",
addr,
if !name.as_ref().is_empty() {
name.as_ref()
} else {
"<unset>"
},
if !name.is_empty() { name } else { "<unset>" },
);
bail!("Bad address supplied: {:?}", addr);
}
let mut name = name.as_ref();
let mut name = name;
#[allow(clippy::collapsible_if)]
if origin <= Origin::OutgoingTo {
// The user may accidentally have written to a "noreply" address with another MUA:
@@ -419,26 +414,33 @@ impl Contact {
let mut update_addr = false;
let mut row_id = 0;
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context.sql.query_row(
"SELECT id, name, addr, origin, authname FROM contacts WHERE addr=? COLLATE NOCASE;",
paramsv![addr.to_string()],
|row| {
let row_id = row.get(0)?;
let row_name: String = row.get(1)?;
let row_addr: String = row.get(2)?;
let row_origin: Origin = row.get(3)?;
let row_authname: String = row.get(4)?;
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context
.sql
.query_row(
"SELECT id, name, addr, origin, authname \
FROM contacts WHERE addr=? COLLATE NOCASE;",
paramsv![addr.to_string()],
|row| {
let row_id: isize = row.get(0)?;
let row_name: String = row.get(1)?;
let row_addr: String = row.get(2)?;
let row_origin: Origin = row.get(3)?;
let row_authname: String = row.get(4)?;
Ok((row_id, row_name, row_addr, row_origin, row_authname))
},
)
.await {
Ok((row_id, row_name, row_addr, row_origin, row_authname))
},
)
.await
{
let update_name = manual && name != row_name;
let update_authname =
!manual && name != row_authname && !name.is_empty() &&
(origin >= row_origin || origin == Origin::IncomingUnknownFrom || row_authname.is_empty());
let update_authname = !manual
&& name != row_authname
&& !name.is_empty()
&& (origin >= row_origin
|| origin == Origin::IncomingUnknownFrom
|| row_authname.is_empty());
row_id = id;
row_id = u32::try_from(id)?;
if origin as i32 >= row_origin as i32 && addr != row_addr {
update_addr = true;
}
@@ -455,7 +457,11 @@ impl Contact {
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
paramsv![
new_name,
if update_addr { addr.to_string() } else { row_addr },
if update_addr {
addr.to_string()
} else {
row_addr
},
if origin > row_origin {
origin
} else {
@@ -475,17 +481,29 @@ impl Contact {
if update_name {
// Update the contact name also if it is used as a group name.
// This is one of the few duplicated data, however, getting the chat list is easier this way.
let chat_id = context.sql.query_get_value::<i32>(
context,
let chat_id: Option<i32> = context.sql.query_get_value(
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
paramsv![Chattype::Single, row_id]
).await;
paramsv![Chattype::Single, isize::try_from(row_id)?]
).await?;
if let Some(chat_id) = chat_id {
match context.sql.execute("UPDATE chats SET name=? WHERE id=? AND name!=?1", paramsv![new_name, chat_id]).await {
let contact = Contact::get_by_id(context, row_id as u32).await?;
let chat_name = contact.get_display_name();
match context
.sql
.execute(
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?3",
paramsv![chat_name, chat_id, chat_name],
)
.await
{
Err(err) => warn!(context, "Can't update chat name: {}", err),
Ok(count) => if count > 0 {
// Chat name updated
context.emit_event(EventType::ChatModified(ChatId::new(chat_id as u32)));
Ok(count) => {
if count > 0 {
// Chat name updated
context.emit_event(EventType::ChatModified(ChatId::new(
chat_id.try_into()?,
)));
}
}
}
}
@@ -496,24 +514,28 @@ impl Contact {
let update_name = manual;
let update_authname = !manual;
if context
if let Ok(new_row_id) = context
.sql
.execute(
.insert(
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
paramsv![
if update_name { name.to_string() } else { "".to_string() },
if update_name {
name.to_string()
} else {
"".to_string()
},
addr,
origin,
if update_authname { name.to_string() } else { "".to_string() }
if update_authname {
name.to_string()
} else {
"".to_string()
}
],
)
.await
.is_ok()
{
row_id = context
.sql
.get_rowid(context, "contacts", "addr", &addr)
.await?;
row_id = u32::try_from(new_row_id)?;
sth_modified = Modifier::Created;
info!(context, "added contact id={} addr={}", row_id, &addr);
} else {
@@ -541,13 +563,13 @@ impl Contact {
/// The `addr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`.
///
/// Returns the number of modified contacts.
pub async fn add_address_book(context: &Context, addr_book: impl AsRef<str>) -> Result<usize> {
pub async fn add_address_book(context: &Context, addr_book: &str) -> Result<usize> {
let mut modify_cnt = 0;
for (name, addr) in split_address_book(addr_book.as_ref()).into_iter() {
for (name, addr) in split_address_book(addr_book).into_iter() {
let (name, addr) = sanitize_name_and_addr(name, addr);
let name = normalize_name(name);
match Contact::add_or_lookup(context, name, &addr, Origin::AddressBook).await {
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
Err(err) => {
warn!(
context,
@@ -584,7 +606,7 @@ impl Contact {
) -> Result<Vec<u32>> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
let mut add_self = false;
@@ -593,13 +615,7 @@ impl Contact {
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
if flag_verified_only || query.is_some() {
let s3str_like_cmd = format!(
"%{}%",
query
.as_ref()
.map(|s| s.as_ref().to_string())
.unwrap_or_default()
);
let s3str_like_cmd = format!("%{}%", query.as_ref().map(|s| s.as_ref()).unwrap_or(""));
context
.sql
.query_map(
@@ -632,7 +648,7 @@ impl Contact {
let self_name = context
.get_config(Config::Displayname)
.await
.await?
.unwrap_or_default();
let self_name2 = stock_str::self_msg(context);
@@ -658,7 +674,11 @@ impl Contact {
AND origin>=?3
AND blocked=0
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
paramsv![self_addr, DC_CONTACT_ID_LAST_SPECIAL as i32, 0x100],
paramsv![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
Origin::IncomingReplyTo
],
|row| row.get::<_, i32>(0),
|ids| {
for id in ids {
@@ -698,7 +718,10 @@ impl Contact {
for (name, grpid) in blocked_mailinglists {
if !context
.sql
.exists("SELECT id FROM contacts WHERE addr=?;", paramsv![grpid])
.exists(
"SELECT COUNT(id) FROM contacts WHERE addr=?;",
paramsv![grpid],
)
.await?
{
context
@@ -718,6 +741,17 @@ impl Contact {
Ok(())
}
pub async fn get_blocked_cnt(context: &Context) -> Result<usize> {
let count = context
.sql
.count(
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
paramsv![DC_CONTACT_ID_LAST_SPECIAL],
)
.await?;
Ok(count as usize)
}
/// Get blocked contacts.
pub async fn get_all_blocked(context: &Context) -> Result<Vec<u32>> {
if let Err(e) = Contact::update_blocked_mailinglist_contacts(context).await {
@@ -727,7 +761,7 @@ impl Contact {
);
}
let ret = context
let list = context
.sql
.query_map(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
@@ -739,7 +773,7 @@ impl Contact {
},
)
.await?;
Ok(ret)
Ok(list)
}
/// Returns a textual summary of the encryption state for the contact.
@@ -755,7 +789,7 @@ impl Contact {
let mut ret = String::new();
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let loginparam = LoginParam::from_database(context, "configured_").await;
let loginparam = LoginParam::from_database(context, "configured_").await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
if let Some(peerstate) = peerstate.filter(|peerstate| {
@@ -791,14 +825,14 @@ impl Contact {
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
cat_fingerprint(
&mut ret,
peerstate.addr.clone(),
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
} else {
cat_fingerprint(
&mut ret,
peerstate.addr.clone(),
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
@@ -822,31 +856,15 @@ impl Contact {
"Can not delete special contact"
);
let count_contacts: i32 = context
let count_chats = context
.sql
.query_get_value(
context,
.count(
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
paramsv![contact_id as i32],
)
.await
.unwrap_or_default();
.await?;
let count_msgs: i32 = if count_contacts > 0 {
context
.sql
.query_get_value(
context,
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
paramsv![contact_id as i32, contact_id as i32],
)
.await
.unwrap_or_default()
} else {
0
};
if count_msgs == 0 {
if count_chats == 0 {
match context
.sql
.execute(
@@ -861,16 +879,16 @@ impl Contact {
}
Err(err) => {
error!(context, "delete_contact {} failed ({})", contact_id, err);
return Err(err.into());
return Err(err);
}
}
}
info!(
context,
"could not delete contact {}, there are {} messages with it", contact_id, count_msgs
"could not delete contact {}, there are {} chats with it", contact_id, count_chats
);
bail!("Could not delete contact with messages in it");
bail!("Could not delete contact with ongoing chats");
}
/// Get a single contact object. For a list, see eg. dc_get_contacts().
@@ -967,17 +985,17 @@ impl Contact {
/// Get the contact's profile image.
/// This is the image set by each remote user on their own
/// using dc_set_config(context, "selfavatar", image).
pub async fn get_profile_image(&self, context: &Context) -> Option<PathBuf> {
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
if self.id == DC_CONTACT_ID_SELF {
if let Some(p) = context.get_config(Config::Selfavatar).await {
return Some(PathBuf::from(p));
if let Some(p) = context.get_config(Config::Selfavatar).await? {
return Ok(Some(PathBuf::from(p)));
}
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Some(dc_get_abs_path(context, image_rel));
return Ok(Some(dc_get_abs_path(context, image_rel)));
}
}
None
Ok(None)
}
/// Get a color for the contact.
@@ -1044,18 +1062,14 @@ impl Contact {
VerifiedStatus::Unverified
}
pub async fn addr_equals_contact(
context: &Context,
addr: impl AsRef<str>,
contact_id: u32,
) -> bool {
if addr.as_ref().is_empty() {
pub async fn addr_equals_contact(context: &Context, addr: &str, contact_id: u32) -> bool {
if addr.is_empty() {
return false;
}
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
if !contact.addr.is_empty() {
let normalized_addr = addr_normalize(addr.as_ref());
let normalized_addr = addr_normalize(addr);
if contact.addr == normalized_addr {
return true;
}
@@ -1065,20 +1079,19 @@ impl Contact {
false
}
pub async fn get_real_cnt(context: &Context) -> usize {
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
if !context.sql.is_open().await {
return 0;
return Ok(0);
}
context
let count = context
.sql
.query_get_value::<isize>(
context,
.count(
"SELECT COUNT(*) FROM contacts WHERE id>?;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
)
.await
.unwrap_or_default() as usize
.await?;
Ok(count)
}
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
@@ -1089,7 +1102,7 @@ impl Contact {
context
.sql
.exists(
"SELECT id FROM contacts WHERE id=?;",
"SELECT COUNT(*) FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
)
.await
@@ -1125,65 +1138,79 @@ pub fn addr_normalize(addr: &str) -> &str {
}
}
fn sanitize_name_and_addr(name: impl AsRef<str>, addr: impl AsRef<str>) -> (String, String) {
fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.as_ref().is_empty() {
if name.is_empty() {
captures
.get(1)
.map_or("".to_string(), |m| normalize_name(m.as_str()))
} else {
name.as_ref().to_string()
name.to_string()
},
captures
.get(2)
.map_or("".to_string(), |m| m.as_str().to_string()),
)
} else {
(name.as_ref().to_string(), addr.as_ref().to_string())
(name.to_string(), addr.to_string())
}
}
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return;
}
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) -> Result<()> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can't block special contact {}",
contact_id
);
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
if contact.blocked != new_blocking
&& context
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![new_blocking as i32, contact_id as i32],
)
.await
.is_ok()
let contact = Contact::load_from_db(context, contact_id).await?;
if contact.blocked != new_blocking {
context
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![new_blocking as i32, contact_id as i32],
)
.await?;
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
// non-destructive blocking->unblocking.
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if context
.sql
.execute(
r#"
UPDATE chats
SET blocked=?
WHERE type=? AND id IN (
SELECT chat_id FROM chats_contacts WHERE contact_id=?
);
"#,
paramsv![new_blocking, Chattype::Single, contact_id],
)
.await
.is_ok()
{
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
// non-destructive blocking->unblocking.
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if context.sql.execute(
"UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
paramsv![new_blocking, 100, contact_id as i32]).await.is_ok()
{
Contact::mark_noticed(context, contact_id).await;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
Contact::mark_noticed(context, contact_id).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Ok((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await
{
chat_id.set_blocked(context, Blocked::Not).await;
}
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await?
{
chat_id.unblock(context).await?;
}
}
}
Ok(())
}
/// Set profile image for a contact.
@@ -1236,13 +1263,31 @@ pub(crate) async fn set_profile_image(
}
/// Sets contact status.
pub(crate) async fn set_status(context: &Context, contact_id: u32, status: String) -> Result<()> {
let mut contact = Contact::load_from_db(context, contact_id).await?;
///
/// For contact SELF, the status is not saved in the contact table, but as Config::Selfstatus. This
/// is only done if message is sent from Delta Chat and it is encrypted, to synchronize signature
/// between Delta Chat devices.
pub(crate) async fn set_status(
context: &Context,
contact_id: u32,
status: String,
encrypted: bool,
has_chat_version: bool,
) -> Result<()> {
if contact_id == DC_CONTACT_ID_SELF {
if encrypted && has_chat_version {
context
.set_config(Config::Selfstatus, Some(&status))
.await?;
}
} else {
let mut contact = Contact::load_from_db(context, contact_id).await?;
if contact.status != status {
contact.status = status;
contact.update_status(context).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
if contact.status != status {
contact.status = status;
contact.update_status(context).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
}
Ok(())
}
@@ -1269,13 +1314,13 @@ pub fn normalize_name(full_name: impl AsRef<str>) -> String {
fn cat_fingerprint(
ret: &mut String,
addr: impl AsRef<str>,
addr: &str,
fingerprint_verified: impl AsRef<str>,
fingerprint_unverified: impl AsRef<str>,
) {
*ret += &format!(
"\n\n{}:\n{}",
addr.as_ref(),
addr,
if !fingerprint_verified.as_ref().is_empty() {
fingerprint_verified.as_ref()
} else {
@@ -1288,7 +1333,7 @@ fn cat_fingerprint(
{
*ret += &format!(
"\n\n{} (alternative):\n{}",
addr.as_ref(),
addr,
fingerprint_unverified.as_ref()
);
}
@@ -1299,7 +1344,7 @@ impl Context {
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
let self_addr = self
.get_config(Config::ConfiguredAddr)
.await
.await?
.ok_or_else(|| format_err!("Not configured"))?;
Ok(addr_cmp(self_addr, addr))
@@ -1333,6 +1378,7 @@ mod tests {
use super::*;
use crate::chat::send_text_msg;
use crate::message::Message;
use crate::test_utils::TestContext;
#[test]
@@ -1341,12 +1387,12 @@ mod tests {
assert_eq!(may_be_valid_addr("user@domain.tld"), true);
assert_eq!(may_be_valid_addr("uuu"), false);
assert_eq!(may_be_valid_addr("dd.tt"), false);
assert_eq!(may_be_valid_addr("tt.dd@uu"), false);
assert_eq!(may_be_valid_addr("u@d"), false);
assert_eq!(may_be_valid_addr("u@d."), false);
assert_eq!(may_be_valid_addr("u@d.t"), false);
assert_eq!(may_be_valid_addr("tt.dd@uu"), true);
assert_eq!(may_be_valid_addr("u@d"), true);
assert_eq!(may_be_valid_addr("u@d."), true);
assert_eq!(may_be_valid_addr("u@d.t"), true);
assert_eq!(may_be_valid_addr("u@d.tt"), true);
assert_eq!(may_be_valid_addr("u@.tt"), false);
assert_eq!(may_be_valid_addr("u@.tt"), true);
assert_eq!(may_be_valid_addr("@d.tt"), false);
assert_eq!(may_be_valid_addr("<da@d.tt"), false);
assert_eq!(may_be_valid_addr("sk <@d.tt>"), false);
@@ -1559,6 +1605,34 @@ mod tests {
assert!(!contact.is_blocked());
}
#[async_std::test]
async fn test_delete() -> Result<()> {
let alice = TestContext::new_alice().await;
assert!(Contact::delete(&alice, DC_CONTACT_ID_SELF).await.is_err());
// Create Bob contact
let (contact_id, _) =
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
.await
.unwrap();
let chat = alice
.create_chat_with_contact("Bob", "bob@example.net")
.await;
// Can't delete a contact with ongoing chats.
assert!(Contact::delete(&alice, contact_id).await.is_err());
// Delete chat.
chat.get_id().delete(&alice).await?;
// Can delete contact now.
Contact::delete(&alice, contact_id).await?;
Ok(())
}
#[async_std::test]
async fn test_remote_authnames() {
let t = TestContext::new().await;
@@ -1762,9 +1836,6 @@ mod tests {
assert!(Contact::create(&t, "", "dskjfdslk@sadklj.dk>")
.await
.is_err());
assert!(Contact::create(&t, "", "dskjf@dslk@sadkljdk")
.await
.is_err());
assert!(Contact::create(&t, "", "dskjf dslk@d.e").await.is_err());
assert!(Contact::create(&t, "", "<dskjf dslk@sadklj.dk")
.await
@@ -1838,4 +1909,70 @@ CCCB 5AA9 F6E1 141C 9431
Ok(())
}
/// Tests that status is synchronized when sending encrypted BCC-self messages and not
/// synchronized when the message is not encrypted.
#[async_std::test]
async fn test_synchronize_status() -> Result<()> {
// Alice has two devices.
let alice1 = TestContext::new_alice().await;
let alice2 = TestContext::new_alice().await;
// Bob has one device.
let bob = TestContext::new_bob().await;
let default_status = alice1.get_config(Config::Selfstatus).await?;
alice1
.set_config(Config::Selfstatus, Some("New status"))
.await?;
let chat = alice1
.create_chat_with_contact("Bob", "bob@example.net")
.await;
// Alice sends a message to Bob from the first device.
send_text_msg(&alice1, chat.id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// Message is not encrypted.
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
assert!(!message.get_showpadlock());
// Alice's second devices receives a copy of outgoing message.
alice2.recv_msg(&sent_msg).await;
// Bob receives message.
bob.recv_msg(&sent_msg).await;
// Message was not encrypted, so status is not copied.
assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status);
// Bob replies.
let chat = bob
.create_chat_with_contact("Alice", "alice@example.com")
.await;
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
alice1.recv_msg(&sent_msg).await;
alice2.recv_msg(&sent_msg).await;
// Alice sends second message.
send_text_msg(&alice1, chat.id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// Second message is encrypted.
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
assert!(message.get_showpadlock());
// Alice's second devices receives a copy of second outgoing message.
alice2.recv_msg(&sent_msg).await;
assert_eq!(
alice2.get_config(Config::Selfstatus).await?,
Some("New status".to_string())
);
Ok(())
}
}

View File

@@ -22,6 +22,7 @@ use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::scheduler::Scheduler;
use crate::securejoin::Bob;
use crate::sql::Sql;
@@ -62,6 +63,10 @@ pub struct InnerContext {
pub(crate) scheduler: RwLock<Scheduler>,
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
/// Recently loaded quota information, if any.
/// Set to `None` if quota was never tried to load.
pub(crate) quota: RwLock<Option<QuotaInfo>>,
pub(crate) last_full_folder_scan: Mutex<Option<Instant>>,
/// ID for this `Context` in the current process.
@@ -91,6 +96,7 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
res.insert("num_cpus", num_cpus::get().to_string());
res.insert("level", "awesome".into());
res
}
@@ -138,6 +144,7 @@ impl Context {
events: Events::default(),
scheduler: RwLock::new(Scheduler::Stopped),
ephemeral_task: RwLock::new(None),
quota: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
};
@@ -160,7 +167,9 @@ impl Context {
{
let l = &mut *self.inner.scheduler.write().await;
l.start(self.clone()).await;
if let Err(err) = l.start(self.clone()).await {
error!(self, "Failed to start IO: {}", err)
}
}
}
@@ -270,71 +279,71 @@ impl Context {
* UI chat/message related API
******************************************************************************/
pub async fn get_info(&self) -> BTreeMap<&'static str, String> {
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let unset = "0";
let l = LoginParam::from_database(self, "").await;
let l2 = LoginParam::from_database(self, "configured_").await;
let displayname = self.get_config(Config::Displayname).await;
let chats = get_chat_cnt(self).await as usize;
let real_msgs = message::get_real_msg_cnt(self).await as usize;
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize;
let contacts = Contact::get_real_cnt(self).await as usize;
let is_configured = self.get_config_int(Config::Configured).await;
let l = LoginParam::from_database(self, "").await?;
let l2 = LoginParam::from_database(self, "configured_").await?;
let displayname = self.get_config(Config::Displayname).await?;
let chats = get_chat_cnt(self).await? as usize;
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await as usize;
let request_msgs = message::get_request_msg_cnt(self).await as usize;
let contacts = Contact::get_real_cnt(self).await? as usize;
let is_configured = self.get_config_int(Config::Configured).await?;
let dbversion = self
.sql
.get_raw_config_int(self, "dbversion")
.await
.get_raw_config_int("dbversion")
.await?
.unwrap_or_default();
let journal_mode = self
.sql
.query_get_value(self, "PRAGMA journal_mode;", paramsv![])
.await
.query_get_value("PRAGMA journal_mode;", paramsv![])
.await?
.unwrap_or_else(|| "unknown".to_string());
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await;
let bcc_self = self.get_config_int(Config::BccSelf).await;
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
let bcc_self = self.get_config_int(Config::BccSelf).await?;
let prv_key_cnt: Option<isize> = self
let prv_key_cnt = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
.await;
.count("SELECT COUNT(*) FROM keypairs;", paramsv![])
.await?;
let pub_key_cnt: Option<isize> = self
let pub_key_cnt = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
.await;
.count("SELECT COUNT(*) FROM acpeerstates;", paramsv![])
.await?;
let fingerprint_str = match SignedPublicKey::load_self(self).await {
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {}>", err),
};
let inbox_watch = self.get_config_int(Config::InboxWatch).await;
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await;
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await;
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
let sentbox_move = self.get_config_int(Config::SentboxMove).await;
let inbox_watch = self.get_config_int(Config::InboxWatch).await?;
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await?;
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
let folders_configured = self
.sql
.get_raw_config_int(self, "folders_configured")
.await
.get_raw_config_int("folders_configured")
.await?
.unwrap_or_default();
let configured_sentbox_folder = self
.get_config(Config::ConfiguredSentboxFolder)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self
.get_config(Config::ConfiguredMvboxFolder)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info();
// insert values
res.insert("bot", self.get_config_int(Config::Bot).await.to_string());
res.insert("bot", self.get_config_int(Config::Bot).await?.to_string());
res.insert("number_of_chats", chats.to_string());
res.insert("number_of_chat_messages", real_msgs.to_string());
res.insert("messages_in_contact_requests", deaddrop_msgs.to_string());
res.insert("number_of_chat_messages", unblocked_msgs.to_string());
res.insert("messages_in_contact_requests", request_msgs.to_string());
res.insert("number_of_contacts", contacts.to_string());
res.insert("database_dir", self.get_dbfile().display().to_string());
res.insert("database_version", dbversion.to_string());
@@ -344,7 +353,7 @@ impl Context {
res.insert(
"selfavatar",
self.get_config(Config::Selfavatar)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string());
@@ -353,12 +362,12 @@ impl Context {
res.insert(
"fetch_existing_msgs",
self.get_config_int(Config::FetchExistingMsgs)
.await
.await?
.to_string(),
);
res.insert(
"show_emails",
self.get_config_int(Config::ShowEmails).await.to_string(),
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert("inbox_watch", inbox_watch.to_string());
res.insert("sentbox_watch", sentbox_watch.to_string());
@@ -372,67 +381,61 @@ impl Context {
res.insert("e2ee_enabled", e2ee_enabled.to_string());
res.insert(
"key_gen_type",
self.get_config_int(Config::KeyGenType).await.to_string(),
self.get_config_int(Config::KeyGenType).await?.to_string(),
);
res.insert("bcc_self", bcc_self.to_string());
res.insert(
"private_key_count",
prv_key_cnt.unwrap_or_default().to_string(),
);
res.insert(
"public_key_count",
pub_key_cnt.unwrap_or_default().to_string(),
);
res.insert("private_key_count", prv_key_cnt.to_string());
res.insert("public_key_count", pub_key_cnt.to_string());
res.insert("fingerprint", fingerprint_str);
res.insert(
"webrtc_instance",
self.get_config(Config::WebrtcInstance)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert(
"media_quality",
self.get_config_int(Config::MediaQuality).await.to_string(),
self.get_config_int(Config::MediaQuality).await?.to_string(),
);
res.insert(
"delete_device_after",
self.get_config_int(Config::DeleteDeviceAfter)
.await
.await?
.to_string(),
);
res.insert(
"delete_server_after",
self.get_config_int(Config::DeleteServerAfter)
.await
.await?
.to_string(),
);
res.insert(
"last_housekeeping",
self.get_config_int(Config::LastHousekeeping)
.await
.await?
.to_string(),
);
res.insert(
"scan_all_folders_debounce_secs",
self.get_config_int(Config::ScanAllFoldersDebounceSecs)
.await
.await?
.to_string(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
res
Ok(res)
}
/// Get a list of fresh, unmuted messages in any chat but deaddrop.
/// Get a list of fresh, unmuted messages in unblocked chats.
///
/// The list starts with the most recent message
/// and is typically used to show notifications.
/// Moreover, the number of returned messages
/// can be used for a badge counter on the app icon.
pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
let ret = self
let list = self
.sql
.query_map(
concat!(
@@ -453,28 +456,27 @@ impl Context {
paramsv![MessageState::InFresh, time()],
|row| row.get::<_, MsgId>(0),
|rows| {
let mut ret = Vec::new();
let mut list = Vec::new();
for row in rows {
ret.push(row?);
list.push(row?);
}
Ok(ret)
Ok(list)
},
)
.await?;
Ok(ret)
Ok(list)
}
/// Searches for messages containing the query string.
///
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
/// is `None` this searches messages from all chats.
pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: impl AsRef<str>) -> Vec<MsgId> {
let real_query = query.as_ref().trim();
pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: &str) -> Result<Vec<MsgId>> {
let real_query = query.trim();
if real_query.is_empty() {
return Vec::new();
return Ok(Vec::new());
}
let str_like_in_text = format!("%{}%", real_query);
let str_like_beg = format!("{}%", real_query);
let do_query = |query, params| {
self.sql.query_map(
@@ -491,7 +493,7 @@ impl Context {
)
};
if let Some(chat_id) = chat_id {
let list = if let Some(chat_id) = chat_id {
do_query(
"SELECT m.id AS id, m.timestamp AS timestamp
FROM msgs m
@@ -500,13 +502,22 @@ impl Context {
WHERE m.chat_id=?
AND m.hidden=0
AND ct.blocked=0
AND (txt LIKE ? OR ct.name LIKE ?)
AND txt LIKE ?
ORDER BY m.timestamp,m.id;",
paramsv![chat_id, str_like_in_text, str_like_beg],
paramsv![chat_id, str_like_in_text],
)
.await
.unwrap_or_default()
.await?
} else {
// For performance reasons results are sorted only by `id`, that is in the order of
// message reception.
//
// Unlike chat view, sorting by `timestamp` is not necessary but slows down the query by
// ~25% according to benchmarks.
//
// To speed up incremental search, where queries for few characters usually return lots
// of unwanted results that are discarded moments later, we added `LIMIT 1000`.
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
// The limit is documented and UI may add a hint when getting 1000 results.
do_query(
"SELECT m.id AS id, m.timestamp AS timestamp
FROM msgs m
@@ -518,33 +529,34 @@ impl Context {
AND m.hidden=0
AND c.blocked=0
AND ct.blocked=0
AND (m.txt LIKE ? OR ct.name LIKE ?)
ORDER BY m.timestamp DESC,m.id DESC;",
paramsv![str_like_in_text, str_like_beg],
AND m.txt LIKE ?
ORDER BY m.id DESC LIMIT 1000",
paramsv![str_like_in_text],
)
.await
.unwrap_or_default()
}
.await?
};
Ok(list)
}
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredInboxFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_inbox(&self, folder_name: &str) -> Result<bool> {
let inbox = self.get_config(Config::ConfiguredInboxFolder).await?;
Ok(inbox.as_deref() == Some(folder_name))
}
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredSentboxFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_sentbox(&self, folder_name: &str) -> Result<bool> {
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
Ok(sentbox.as_deref() == Some(folder_name))
}
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredMvboxFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
Ok(mvbox.as_deref() == Some(folder_name))
}
pub async fn is_spam_folder(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredSpamFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_spam_folder(&self, folder_name: &str) -> Result<bool> {
let spam = self.get_config(Config::ConfiguredSpamFolder).await?;
Ok(spam.as_deref() == Some(folder_name))
}
pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
@@ -553,6 +565,13 @@ impl Context {
blob_fname.push("-blobs");
dbfile.with_file_name(blob_fname)
}
pub fn derive_walfile(dbfile: &PathBuf) -> PathBuf {
let mut wal_fname = OsString::new();
wal_fname.push(dbfile.file_name().unwrap_or_default());
wal_fname.push("-wal");
dbfile.with_file_name(wal_fname)
}
}
impl InnerContext {
@@ -596,9 +615,13 @@ pub fn get_version_str() -> &'static str {
mod tests {
use super::*;
use crate::chat::{get_chat_contacts, get_chat_msgs, set_muted, Chat, MuteDuration};
use crate::chat::{
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
};
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
use crate::message::Message;
use crate::test_utils::TestContext;
use std::time::Duration;
use strum::IntoEnumIterator;
@@ -620,7 +643,7 @@ mod tests {
}
async fn receive_msg(t: &TestContext, chat: &Chat) {
let members = get_chat_contacts(t, chat.id).await;
let members = get_chat_contacts(t, chat.id).await.unwrap();
let contact = Contact::load_from_db(t, *members.first().unwrap())
.await
.unwrap();
@@ -651,43 +674,49 @@ mod tests {
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
receive_msg(&t, &bob).await;
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.len(), 1);
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await, 1);
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
receive_msg(&t, &claire).await;
receive_msg(&t, &claire).await;
assert_eq!(get_chat_msgs(&t, claire.id, 0, None).await.len(), 2);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 2);
assert_eq!(
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
2
);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
receive_msg(&t, &dave).await;
receive_msg(&t, &dave).await;
receive_msg(&t, &dave).await;
assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.len(), 3);
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await, 3);
assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.unwrap().len(), 3);
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
// mute one of the chats
set_muted(&t, claire.id, MuteDuration::Forever)
.await
.unwrap();
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 2);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
// receive more messages
receive_msg(&t, &bob).await;
receive_msg(&t, &claire).await;
receive_msg(&t, &dave).await;
assert_eq!(get_chat_msgs(&t, claire.id, 0, None).await.len(), 3);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 3);
assert_eq!(
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
3
);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
// unmute claire again
set_muted(&t, claire.id, MuteDuration::NotMuted)
.await
.unwrap();
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 3);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
}
@@ -696,7 +725,7 @@ mod tests {
let t = TestContext::new_alice().await;
let bob = t.create_chat_with_contact("", "bob@g.it").await;
receive_msg(&t, &bob).await;
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.len(), 1);
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
// chat is unmuted by default, here and in the following assert(),
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
@@ -811,7 +840,7 @@ mod tests {
async fn test_get_info() {
let t = TestContext::new().await;
let info = t.get_info().await;
let info = t.get_info().await.unwrap();
assert!(info.get("database_dir").is_some());
}
@@ -851,7 +880,7 @@ mod tests {
"smtp_certificate_checks",
];
let t = TestContext::new().await;
let info = t.get_info().await;
let info = t.get_info().await.unwrap();
for key in Config::iter() {
let key: String = key.to_string();
if !skip_from_get_info.contains(&&*key)
@@ -860,9 +889,99 @@ mod tests {
{
assert!(
info.contains_key(&*key),
format!("'{}' missing in get_info() output", key)
"'{}' missing in get_info() output",
key
);
}
}
}
#[async_std::test]
async fn test_search_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
let self_talk = ChatId::create_for_contact(&alice, DC_CONTACT_ID_SELF).await?;
let chat = alice
.create_chat_with_contact("Bob", "bob@example.org")
.await;
// Global search finds nothing.
let res = alice.search_msgs(None, "foo").await?;
assert!(res.is_empty());
// Search in chat with Bob finds nothing.
let res = alice.search_msgs(Some(chat.id), "foo").await?;
assert!(res.is_empty());
// Add messages to chat with Bob.
let mut msg1 = Message::new(Viewtype::Text);
msg1.set_text(Some("foobar".to_string()));
send_msg(&alice, chat.id, &mut msg1).await?;
let mut msg2 = Message::new(Viewtype::Text);
msg2.set_text(Some("barbaz".to_string()));
send_msg(&alice, chat.id, &mut msg2).await?;
// Global search with a part of text finds the message.
let res = alice.search_msgs(None, "ob").await?;
assert_eq!(res.len(), 1);
// Global search for "bar" matches both "foobar" and "barbaz".
let res = alice.search_msgs(None, "bar").await?;
assert_eq!(res.len(), 2);
// Message added later is returned first.
assert_eq!(res.get(0), Some(&msg2.id));
assert_eq!(res.get(1), Some(&msg1.id));
// Global search with longer text does not find any message.
let res = alice.search_msgs(None, "foobarbaz").await?;
assert!(res.is_empty());
// Search for random string finds nothing.
let res = alice.search_msgs(None, "abc").await?;
assert!(res.is_empty());
// Search in chat with Bob finds the message.
let res = alice.search_msgs(Some(chat.id), "foo").await?;
assert_eq!(res.len(), 1);
// Search in Saved Messages does not find the message.
let res = alice.search_msgs(Some(self_talk), "foo").await?;
assert!(res.is_empty());
Ok(())
}
#[async_std::test]
async fn test_limit_search_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat = alice
.create_chat_with_contact("Bob", "bob@example.org")
.await;
// Add 999 messages
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foobar".to_string()));
for _ in 0..999 {
send_msg(&alice, chat.id, &mut msg).await?;
}
let res = alice.search_msgs(None, "foo").await?;
assert_eq!(res.len(), 999);
// Add one more message, no limit yet
send_msg(&alice, chat.id, &mut msg).await?;
let res = alice.search_msgs(None, "foo").await?;
assert_eq!(res.len(), 1000);
// Add one more message, that one is truncated then
send_msg(&alice, chat.id, &mut msg).await?;
let res = alice.search_msgs(None, "foo").await?;
assert_eq!(res.len(), 1000);
// In-chat should not be not limited
let res = alice.search_msgs(Some(chat.id), "foo").await?;
assert_eq!(res.len(), 1001);
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -608,19 +608,8 @@ impl FromStr for EmailAddress {
if local.is_empty() {
return err("empty string is not valid for local part");
}
if domain.len() <= 3 {
return err("domain is too short");
}
let dot = domain.find('.');
match dot {
None => {
return err("invalid domain");
}
Some(dot_idx) => {
if dot_idx >= domain.len() - 2 {
return err("invalid domain");
}
}
if domain.is_empty() {
return err("missing domain after '@'");
}
Ok(EmailAddress {
local: (*local).to_string(),
@@ -641,9 +630,8 @@ impl rusqlite::types::ToSql for EmailAddress {
}
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
pub(crate) fn improve_single_line_input(input: impl AsRef<str>) -> String {
pub(crate) fn improve_single_line_input(input: &str) -> String {
input
.as_ref()
.replace("\n", " ")
.replace("\r", " ")
.trim()
@@ -667,7 +655,7 @@ pub fn remove_subject_prefix(last_subject: &str) -> String {
0
} else {
// "Antw:" is the longest abbreviation in
// https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages,
// <https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages>,
// so look at the first _5_ characters:
match last_subject.chars().take(5).position(|c| c == ':') {
Some(prefix_end) => prefix_end + 1,
@@ -826,12 +814,19 @@ mod tests {
domain: "domain.tld".into(),
}
);
assert_eq!(
"user@localhost".parse::<EmailAddress>().unwrap(),
EmailAddress {
local: "user".into(),
domain: "localhost".into()
}
);
assert_eq!("uuu".parse::<EmailAddress>().is_ok(), false);
assert_eq!("dd.tt".parse::<EmailAddress>().is_ok(), false);
assert_eq!("tt.dd@uu".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d.".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d.t".parse::<EmailAddress>().is_ok(), false);
assert!("tt.dd@uu".parse::<EmailAddress>().is_ok());
assert!("u@d".parse::<EmailAddress>().is_ok());
assert!("u@d.".parse::<EmailAddress>().is_ok());
assert!("u@d.t".parse::<EmailAddress>().is_ok());
assert_eq!(
"u@d.tt".parse::<EmailAddress>().unwrap(),
EmailAddress {
@@ -839,7 +834,7 @@ mod tests {
domain: "d.tt".into(),
}
);
assert_eq!("u@tt".parse::<EmailAddress>().is_ok(), false);
assert!("u@tt".parse::<EmailAddress>().is_ok());
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
}
@@ -887,16 +882,6 @@ mod tests {
}
assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje").await);
if dc_file_exist!(context, "$BLOBDIR/foobar").await
|| dc_file_exist!(context, "$BLOBDIR/dada").await
|| dc_file_exist!(context, "$BLOBDIR/foobar.dadada").await
|| dc_file_exist!(context, "$BLOBDIR/foobar-folder").await
{
dc_delete_file(context, "$BLOBDIR/foobar").await;
dc_delete_file(context, "$BLOBDIR/dada").await;
dc_delete_file(context, "$BLOBDIR/foobar.dadada").await;
dc_delete_file(context, "$BLOBDIR/foobar-folder").await;
}
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content")
.await
.is_ok());
@@ -1053,7 +1038,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// the message should be added only once a day - test that an hour later and nearly a day later
@@ -1063,7 +1050,9 @@ mod tests {
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
maybe_warn_on_bad_time(
@@ -1072,7 +1061,9 @@ mod tests {
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// next day, there should be another device message
@@ -1085,7 +1076,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
assert_eq!(device_chat_id, chats.get_chat_id(0));
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 2);
}
@@ -1115,7 +1108,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// do not repeat the warning every day ...
@@ -1135,7 +1130,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
let test_len = msgs.len();
assert!(test_len == 1 || test_len == 2);
@@ -1150,7 +1147,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), test_len + 1);
}
}

View File

@@ -23,11 +23,14 @@ struct Dehtml {
/// Everything between <div name="quote"> and <div name="quoted-content"> is usually metadata
/// If this is > `0`, then we are inside a `<div name="quoted-content">`.
divs_since_quoted_content_div: u32,
/// All-Inkl just puts the quote into `<blockquote> </blockquote>`. This count is
/// increased at each `<blockquote>` and decreased at each `</blockquote>`.
blockquotes_since_blockquote: u32,
}
impl Dehtml {
fn line_prefix(&self) -> &str {
if self.divs_since_quoted_content_div > 0 {
if self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 {
"> "
} else {
""
@@ -67,7 +70,7 @@ pub fn dehtml(buf: &str) -> Option<String> {
None
}
pub fn dehtml_quick_xml(buf: &str) -> String {
fn dehtml_quick_xml(buf: &str) -> String {
let buf = buf.trim().trim_start_matches("<!doctype html>");
let mut dehtml = Dehtml {
@@ -76,6 +79,7 @@ pub fn dehtml_quick_xml(buf: &str) -> String {
last_href: None,
divs_since_quote_div: 0,
divs_since_quoted_content_div: 0,
blockquotes_since_blockquote: 0,
};
let mut reader = quick_xml::Reader::from_str(buf);
@@ -179,6 +183,7 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
dehtml.strbuilder += "_";
}
}
"blockquote" => pop_tag(&mut dehtml.blockquotes_since_blockquote),
_ => {}
}
}
@@ -241,6 +246,7 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
dehtml.strbuilder += "_";
}
}
"blockquote" => dehtml.blockquotes_since_blockquote += 1,
_ => {}
}
}
@@ -308,6 +314,7 @@ mod tests {
"[ Foo ](https://example.com)",
),
("<b> bar </b>", "* bar *"),
("<i>foo</i>", "_foo_"),
("<b> bar <i> foo", "* bar _ foo"),
("&amp; bar", "& bar"),
// Despite missing ', this should be shown:
@@ -385,6 +392,13 @@ mod tests {
assert_eq!(txt.trim(), "lots of text");
}
#[test]
fn test_pre_tag() {
let input = "<html><pre>\ntwo\nlines\n</pre></html>";
let txt = dehtml(input).unwrap();
assert_eq!(txt.trim(), "two\nlines");
}
#[async_std::test]
async fn test_quote_div() {
let input = include_str!("../test-data/message/gmx-quote-body.eml");

View File

@@ -26,9 +26,9 @@ pub struct EncryptHelper {
impl EncryptHelper {
pub async fn new(context: &Context) -> Result<EncryptHelper> {
let prefer_encrypt =
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await)
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
.unwrap_or_default();
let addr = match context.get_config(Config::ConfiguredAddr).await {
let addr = match context.get_config(Config::ConfiguredAddr).await? {
None => {
bail!("addr not configured!");
}
@@ -178,7 +178,9 @@ pub async fn try_decrypt(
let mut signatures = HashSet::default();
if let Some(ref mut peerstate) = peerstate {
peerstate.handle_fingerprint_change(context).await?;
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
@@ -209,29 +211,58 @@ pub async fn try_decrypt(
Ok((out_mail, signatures))
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
ensure!(
mail.ctype.mimetype == "multipart/encrypted",
"Not a multipart/encrypted message: {}",
mail.ctype.mimetype
);
/// Returns a reference to the encrypted payload of a valid PGP/MIME message.
///
/// Returns `None` if the message is not a valid PGP/MIME message.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/encrypted" {
return None;
}
if let [first_part, second_part] = &mail.subparts[..] {
ensure!(
first_part.ctype.mimetype == "application/pgp-encrypted",
"Invalid Autocrypt Level 1 version part: {:?}",
first_part.ctype,
);
ensure!(
second_part.ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
second_part.ctype
);
Ok(second_part)
if first_part.ctype.mimetype == "application/pgp-encrypted"
&& second_part.ctype.mimetype == "application/octet-stream"
{
Some(second_part)
} else {
None
}
} else {
bail!("Invalid Autocrypt Level 1 Mime Parts")
None
}
}
/// Returns a reference to the encrypted payload of a ["Mixed
/// Up"][pgpmime-message-mangling] message.
///
/// According to [RFC 3156] encrypted messages should have
/// `multipart/encrypted` MIME type and two parts, but Microsoft
/// Exchange and ProtonMail IMAP/SMTP Bridge are known to mangle this
/// structure by changing the type to `multipart/mixed` and prepending
/// an empty part at the start.
///
/// ProtonMail IMAP/SMTP Bridge prepends a part literally saying
/// "Empty Message", so we don't check its contents at all, checking
/// only for `text/plain` type.
///
/// Returns `None` if the message is not a "Mixed Up" message.
///
/// [RFC 3156]: https://www.rfc-editor.org/info/rfc3156
/// [pgpmime-message-mangling]: https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html
fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/mixed" {
return None;
}
if let [first_part, second_part, third_part] = &mail.subparts[..] {
if first_part.ctype.mimetype == "text/plain"
&& second_part.ctype.mimetype == "application/pgp-encrypted"
&& third_part.ctype.mimetype == "application/octet-stream"
{
Some(third_part)
} else {
None
}
} else {
None
}
}
@@ -242,12 +273,12 @@ async fn decrypt_if_autocrypt_message(
public_keyring_for_validate: Keyring<SignedPublicKey>,
ret_valid_signatures: &mut HashSet<Fingerprint>,
) -> Result<Option<Vec<u8>>> {
let encrypted_data_part = match get_autocrypt_mime(mail) {
Err(_) => {
let encrypted_data_part = match get_autocrypt_mime(mail).or_else(|| get_mixed_up_mime(mail)) {
None => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
}
Ok(res) => res,
Some(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
@@ -329,7 +360,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.ok_or_else(|| {
format_err!(concat!(
"Failed to get self address, ",
@@ -515,9 +546,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
let mut peerstates = Vec::new();
peerstates.push((Some(peerstate), addr));
peerstates
vec![(Some(peerstate), addr)]
}
#[async_std::test]
@@ -542,9 +571,31 @@ Sent with my Delta Chat Messenger: https://delta.chat";
assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with missing peerstate
let mut ps = Vec::new();
ps.push((None, "bob@foo.bar"));
let ps = vec![(None, "bob@foo.bar")];
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
}
#[test]
fn test_mixed_up_mime() -> Result<()> {
// "Mixed Up" mail as received when sending an encrypted
// message using Delta Chat Desktop via ProtonMail IMAP/SMTP
// Bridge.
let mixed_up_mime = include_bytes!("../test-data/message/protonmail-mixed-up.eml");
let mail = mailparse::parse_mail(mixed_up_mime)?;
assert!(get_autocrypt_mime(&mail).is_none());
assert!(get_mixed_up_mime(&mail).is_some());
// Same "Mixed Up" mail repaired by Thunderbird 78.9.0.
//
// It added `X-Enigmail-Info: Fixed broken PGP/MIME message`
// header although the repairing is done by the built-in
// OpenPGP support, not Enigmail.
let repaired_mime = include_bytes!("../test-data/message/protonmail-repaired.eml");
let mail = mailparse::parse_mail(repaired_mime)?;
assert!(get_autocrypt_mime(&mail).is_some());
assert!(get_mixed_up_mime(&mail).is_none());
Ok(())
}
}

View File

@@ -61,20 +61,20 @@ use std::num::ParseIntError;
use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{ensure, Error};
use anyhow::{ensure, Context as _, Result};
use async_std::task;
use serde::{Deserialize, Serialize};
use crate::chat::{lookup_by_contact_id, send_msg, ChatId};
use crate::chat::{send_msg, ChatId};
use crate::constants::{
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
};
use crate::context::Context;
use crate::dc_tools::time;
use crate::events::EventType;
use crate::job;
use crate::message::{Message, MessageState, MsgId};
use crate::mimeparser::SystemMessage;
use crate::sql;
use crate::stock_str;
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
@@ -147,10 +147,10 @@ impl rusqlite::types::FromSql for Timer {
impl ChatId {
/// Get ephemeral message timer value in seconds.
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer, Error> {
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> {
let timer = context
.sql
.query_get_value_result(
.query_get_value(
"SELECT ephemeral_timer FROM chats WHERE id=?;",
paramsv![self],
)
@@ -166,7 +166,7 @@ impl ChatId {
self,
context: &Context,
timer: Timer,
) -> Result<(), Error> {
) -> Result<()> {
ensure!(!self.is_special(), "Invalid chat ID");
context
@@ -189,7 +189,7 @@ impl ChatId {
/// Set ephemeral message timer value in seconds.
///
/// If timer value is 0, disable ephemeral message timer.
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<(), Error> {
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<()> {
if timer == self.get_ephemeral_timer(context).await? {
return Ok(());
}
@@ -261,10 +261,10 @@ pub(crate) async fn stock_ephemeral_timer_changed(
impl MsgId {
/// Returns ephemeral message timer value for the message.
pub(crate) async fn ephemeral_timer(self, context: &Context) -> crate::sql::Result<Timer> {
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> {
let res = match context
.sql
.query_get_value_result(
.query_get_value(
"SELECT ephemeral_timer FROM msgs WHERE id=?",
paramsv![self],
)
@@ -277,7 +277,7 @@ impl MsgId {
}
/// Starts ephemeral message timer for the message if it is not started yet.
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> crate::sql::Result<()> {
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> {
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
let ephemeral_timestamp = time() + i64::from(duration);
@@ -304,32 +304,35 @@ impl MsgId {
/// false. This function does not emit the MsgsChanged event itself,
/// because it is also called when chatlist is reloaded, and emitting
/// MsgsChanged there will cause infinite reload loop.
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, Error> {
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool> {
let mut updated = context
.sql
.execute(
// If you change which information is removed here, also change MsgId::trash() and
// which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH
"UPDATE msgs \
SET chat_id=?, txt='', subject='', txt_raw='', mime_headers='', from_id=0, to_id=0, param='' \
WHERE \
ephemeral_timestamp != 0 \
AND ephemeral_timestamp <= ? \
AND chat_id != ?",
r#"
UPDATE msgs
SET
chat_id=?, txt='', subject='', txt_raw='',
mime_headers='', from_id=0, to_id=0, param=''
WHERE
ephemeral_timestamp != 0
AND ephemeral_timestamp <= ?
AND chat_id != ?
"#,
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
)
.await?
.await
.context("update failed")?
> 0;
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.await
.unwrap_or_default()
.0;
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.await
.unwrap_or_default()
.0;
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
let self_chat_id = ChatId::lookup_by_contact(context, DC_CONTACT_ID_SELF)
.await?
.unwrap_or_default();
let device_chat_id = ChatId::lookup_by_contact(context, DC_CONTACT_ID_DEVICE)
.await?
.unwrap_or_default();
let threshold_timestamp = time() - delete_device_after;
@@ -354,7 +357,8 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
device_chat_id
],
)
.await?;
.await
.context("deleted update failed")?;
updated |= rows_modified > 0;
}
@@ -376,13 +380,15 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
pub async fn schedule_ephemeral_task(context: &Context) {
let ephemeral_timestamp: Option<i64> = match context
.sql
.query_get_value_result(
"SELECT ephemeral_timestamp \
FROM msgs \
WHERE ephemeral_timestamp != 0 \
AND chat_id != ? \
ORDER BY ephemeral_timestamp ASC \
LIMIT 1",
.query_get_value(
r#"
SELECT ephemeral_timestamp
FROM msgs
WHERE ephemeral_timestamp != 0
AND chat_id != ?
ORDER BY ephemeral_timestamp ASC
LIMIT 1;
"#,
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
)
.await
@@ -436,10 +442,10 @@ pub async fn schedule_ephemeral_task(context: &Context) {
///
/// It looks up the trash chat too, to find messages that are already
/// deleted locally, but not deleted on the server.
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Result<Option<MsgId>> {
let now = time();
let threshold_timestamp = match context.get_config_delete_server_after().await {
let threshold_timestamp = match context.get_config_delete_server_after().await? {
None => 0,
Some(delete_server_after) => now - delete_server_after,
};
@@ -453,9 +459,13 @@ pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<O
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
) \
AND server_uid != 0 \
AND NOT id IN (SELECT foreign_id FROM jobs WHERE action = ?)
LIMIT 1",
paramsv![threshold_timestamp, now],
|row| row.get::<_, MsgId>(0),
paramsv![threshold_timestamp, now, job::Action::DeleteMsgOnImap],
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
},
)
.await
}
@@ -469,7 +479,7 @@ pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<O
///
/// This function is supposed to be called in the background,
/// e.g. from housekeeping task.
pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()> {
pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
context
.sql
.execute(
@@ -492,6 +502,7 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()>
#[cfg(test)]
mod tests {
use crate::param::Params;
use async_std::task::sleep;
use super::*;
@@ -713,11 +724,38 @@ mod tests {
sleep(Duration::from_millis(1100)).await;
// Check checks that the msg was deleted locally
check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await;
// Check that the msg will be deleted on the server
// First of all, set a server_uid so that DC thinks that it's actually possible to delete
t.sql
.execute(
"UPDATE msgs SET server_uid=1 WHERE id=?",
paramsv![msg.sender_msg_id],
)
.await
.unwrap();
let job = job::load_imap_deletion_job(&t).await.unwrap();
assert_eq!(
job,
Some(job::Job::new(
job::Action::DeleteMsgOnImap,
msg.sender_msg_id.to_u32(),
Params::new(),
0,
))
);
// Let's assume that executing the job fails on first try and the job is saved to the db
job.unwrap().save(&t).await.unwrap();
// Make sure that we don't get yet another job when loading from db
let job2 = job::load_imap_deletion_job(&t).await.unwrap();
assert_eq!(job2, None);
}
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await;
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await.unwrap();
// Check that the chat is empty except for possibly info messages:
for item in &chat_items {
if let ChatItem::Message { msg_id } = item {
@@ -730,12 +768,13 @@ mod tests {
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
assert_eq!(msg.from_id, 0);
assert_eq!(msg.to_id, 0);
assert!(msg.text.is_none_or_empty(), msg.text);
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
let rawtxt: Option<String> = t
.sql
.query_get_value(t, "SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
.await;
assert!(rawtxt.is_none_or_empty(), rawtxt);
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
.await
.unwrap();
assert!(rawtxt.is_none_or_empty(), "{:?}", rawtxt);
}
}
}

View File

@@ -185,21 +185,6 @@ pub enum EventType {
#[strum(props(id = "400"))]
Error(String),
/// An action cannot be performed because there is no network available.
///
/// The library will typically try over after a some time
/// and when dc_maybe_network() is called.
///
/// Network errors should be reported to users in a non-disturbing way,
/// however, as network errors may come in a sequence,
/// it is not useful to raise each an every error to the user.
///
/// Moreover, if the UI detects that the device is offline,
/// it is probably more useful to report this to the user
/// instead of the string from data2.
#[strum(props(id = "401"))]
ErrorNetwork(String),
/// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to
/// dc_set_chat_name(), dc_set_chat_profile_image(),
@@ -213,6 +198,8 @@ pub enum EventType {
/// - Messages sent, received or removed
/// - Chats created, deleted or archived
/// - A draft has been set
///
/// The `chat_id` and `msg_id` values will be 0 if more than one message is changed.
#[strum(props(id = "2000"))]
MsgsChanged { chat_id: ChatId, msg_id: MsgId },
@@ -328,4 +315,11 @@ pub enum EventType {
/// (Bob has verified alice and waits until Alice does the same for him)
#[strum(props(id = "2061"))]
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
/// and possibly the connectivtiy HTML; see dc_get_connectivity() and
/// dc_get_connectivity_html() for details.
#[strum(props(id = "2100"))]
ConnectivityChanged,
}

View File

@@ -104,13 +104,13 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String {
for line in text.split('\n') {
// Revert space-stuffing
let line = line.strip_prefix(" ").unwrap_or(line);
let line = line.strip_prefix(' ').unwrap_or(line);
if !skip_newline {
result.push('\n');
}
if let Some(line) = line.strip_suffix(" ") {
if let Some(line) = line.strip_suffix(' ') {
// Flowed line
result += line;
if !delsp {
@@ -146,6 +146,17 @@ mod tests {
let text = "> Not a quote";
assert_eq!(format_flowed(text), " > Not a quote");
// Test space stuffing of wrapped lines
let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
> \n\
> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
let expected = "\x20> This is the Autocrypt Setup Message used to transfer your key between \r\n\
clients.\r\n\
\x20>\r\n\
\x20> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
client and enter the setup code presented on the generating device.";
assert_eq!(format_flowed(text), expected);
}
#[test]

View File

@@ -25,6 +25,12 @@ pub enum HeaderDef {
/// we need to check that header as well.
XMicrosoftOriginalMessageId,
/// Thunderbird header used to store Draft information.
///
/// Thunderbird 78.11.0 does not set \Draft flag on messages saved as "Template", but sets this
/// header, so it can be used to ignore such messages.
XMozillaDraftInfo,
ListId,
References,
InReplyTo,

View File

@@ -13,12 +13,12 @@ use std::pin::Pin;
use anyhow::Result;
use lettre_email::mime::{self, Mime};
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::message::{Message, MsgId};
use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml;
use crate::plaintext::PlainText;
use crate::{context::Context, message};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
@@ -244,32 +244,20 @@ impl MsgId {
/// this is the case at least when `Message.has_html()` returns true
/// (we do not save raw mime unconditionally in the database to save space).
/// The corresponding ffi-function is `dc_get_msg_html()`.
pub async fn get_html(self, context: &Context) -> Option<String> {
let rawmime: Option<String> = context
.sql
.query_get_value(
context,
"SELECT mime_headers FROM msgs WHERE id=?;",
paramsv![self],
)
.await;
pub async fn get_html(self, context: &Context) -> Result<Option<String>> {
let rawmime = message::get_mime_headers(context, self).await?;
if let Some(rawmime) = rawmime {
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
Err(err) => {
warn!(context, "get_html: parser error: {}", err);
None
}
Ok(parser) => Some(parser.html),
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, &rawmime).await {
Err(err) => {
warn!(context, "get_html: parser error: {}", err);
Ok(None)
}
} else {
warn!(context, "get_html: empty mime for {}", self);
None
Ok(parser) => Ok(Some(parser.html)),
}
} else {
warn!(context, "get_html: no mime for {}", self);
None
Ok(None)
}
}
}
@@ -436,10 +424,10 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
}
#[async_std::test]
async fn test_get_html_empty() {
async fn test_get_html_invalid_msgid() {
let t = TestContext::new().await;
let msg_id = MsgId::new_unset();
assert!(msg_id.get_html(&t).await.is_none())
let msg_id = MsgId::new(100);
assert!(msg_id.get_html(&t).await.is_err())
}
#[async_std::test]
@@ -460,7 +448,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(!msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
// alice: create chat with bob and forward received html-message there
@@ -474,7 +462,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
// bob: check that bob also got the html-part of the forwarded message
@@ -487,7 +475,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&bob).await.unwrap();
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
@@ -517,7 +505,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
// receive the message on another device
let alice = TestContext::new_alice().await;
assert_eq!(alice.get_config_int(Config::ShowEmails).await, 0); // set to "1" above, make sure it is another db
assert_eq!(alice.get_config_int(Config::ShowEmails).await.unwrap(), 0); // set to "1" above, make sure it is another db
alice.recv_msg(&msg).await;
let chat = alice.get_self_chat().await;
let msg = alice.get_last_msg_in(chat.get_id()).await;
@@ -527,7 +515,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
@@ -549,7 +537,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&alice).await.unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("<b>html</b> text"));
// let bob receive the message
@@ -559,7 +547,29 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&bob).await.unwrap();
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("<b>html</b> text"));
}
#[async_std::test]
async fn test_cp1252_html() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
dc_receive_imf(
&t,
include_bytes!("../test-data/message/cp1252-html.eml"),
"INBOX",
0,
false,
)
.await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.viewtype, Viewtype::Text);
assert!(msg.text.as_ref().unwrap().contains("foo bar ä ö ü ß"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&t).await?.unwrap();
println!("{}", html);
assert!(html.contains("foo bar ä ö ü ß"));
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -32,10 +32,10 @@ impl DerefMut for Client {
}
impl Client {
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
pub async fn login(
self,
username: U,
password: P,
username: &str,
password: &str,
) -> std::result::Result<Session, (ImapError, Self)> {
let Client { inner, is_secure } = self;
let session = inner
@@ -53,10 +53,10 @@ impl Client {
Ok(Session { inner: session })
}
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
pub async fn authenticate(
self,
auth_type: S,
authenticator: A,
auth_type: &str,
authenticator: impl async_imap::Authenticator,
) -> std::result::Result<Session, (ImapError, Self)> {
let Client { inner, is_secure } = self;
let session =
@@ -75,15 +75,14 @@ impl Client {
Ok(Session { inner: session })
}
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
pub async fn connect_secure(
addr: impl net::ToSocketAddrs,
domain: &str,
strict_tls: bool,
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(strict_tls);
let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(domain.as_ref(), stream).await?);
let tls_stream: Box<dyn SessionStream> = Box::new(tls.connect(domain, stream).await?);
let mut client = ImapClient::new(tls_stream);
let _greeting = client
@@ -97,7 +96,7 @@ impl Client {
})
}
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> ImapResult<Self> {
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
let mut client = ImapClient::new(stream);
@@ -112,7 +111,7 @@ impl Client {
})
}
pub async fn secure<S: AsRef<str>>(self, domain: S, strict_tls: bool) -> ImapResult<Client> {
pub async fn secure(self, domain: &str, strict_tls: bool) -> ImapResult<Client> {
if self.is_secure {
Ok(self)
} else {
@@ -121,7 +120,7 @@ impl Client {
inner.run_command_and_check_ok("STARTTLS", None).await?;
let stream = inner.into_inner();
let ssl_stream = tls.connect(domain.as_ref(), stream).await?;
let ssl_stream = tls.connect(domain, stream).await?;
let boxed: Box<dyn SessionStream> = Box::new(ssl_stream);
Ok(Client {

View File

@@ -2,7 +2,6 @@ use super::Imap;
use anyhow::{bail, format_err, Result};
use async_imap::extensions::idle::IdleResponse;
use async_imap::types::UnsolicitedResponse;
use async_std::prelude::*;
use std::time::{Duration, SystemTime};
@@ -25,31 +24,18 @@ impl Imap {
if !self.can_idle() {
bail!("IMAP server does not have IDLE capability");
}
self.setup_handle(context).await?;
self.prepare(context).await?;
self.select_folder(context, watch_folder.clone()).await?;
self.select_folder(context, watch_folder.as_deref()).await?;
let timeout = Duration::from_secs(23 * 60);
let mut info = Default::default();
if self.server_sent_unsolicited_exists(context) {
return Ok(info);
}
if let Some(session) = self.session.take() {
// if we have unsolicited responses we directly return
let mut unsolicited_exists = false;
while let Ok(response) = session.unsolicited_responses.try_recv() {
match response {
UnsolicitedResponse::Exists(_) => {
warn!(context, "skip idle, got unsolicited EXISTS {:?}", response);
unsolicited_exists = true;
}
_ => info!(context, "ignoring unsolicited response {:?}", response),
}
}
if unsolicited_exists {
self.session = Some(session);
return Ok(info);
}
if let Ok(info) = self.idle_interrupt.try_recv() {
info!(context, "skip idle, got interrupt {:?}", info);
self.session = Some(session);
@@ -158,7 +144,7 @@ impl Imap {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.connect_configured(context).await {
if let Err(err) = self.prepare(context).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
@@ -181,7 +167,7 @@ impl Imap {
}
Err(err) => {
error!(context, "could not fetch from folder: {:#}", err);
self.trigger_reconnect()
self.trigger_reconnect(context).await;
}
}
}

View File

@@ -1,13 +1,13 @@
use std::time::Instant;
use std::{collections::BTreeMap, time::Instant};
use anyhow::{Context as _, Result};
use crate::config::Config;
use crate::context::Context;
use crate::imap::Imap;
use crate::{config::Config, log::LogExt};
use crate::{context::Context, imap::FolderMeaning};
use async_std::prelude::*;
use super::{get_folder_meaning, get_folder_meaning_by_name, FolderMeaning};
use super::{get_folder_meaning, get_folder_meaning_by_name};
impl Imap {
pub async fn scan_folders(&mut self, context: &Context) -> Result<()> {
@@ -17,7 +17,7 @@ impl Imap {
let elapsed_secs = last_scan.elapsed().as_secs();
let debounce_secs = context
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
.await;
.await?;
if elapsed_secs < debounce_secs {
return Ok(());
@@ -25,14 +25,13 @@ impl Imap {
}
info!(context, "Starting full folder scan");
self.setup_handle(context).await?;
self.prepare(context).await?;
let session = self.session.as_mut();
let session = session.context("scan_folders(): IMAP No Connection established")?;
let folders: Vec<_> = session.list(Some(""), Some("*")).await?.collect().await;
let watched_folders = get_watched_folders(context).await;
let mut sentbox_folder = None;
let mut spam_folder = None;
let mut folder_configs = BTreeMap::new();
for folder in folders {
let folder = match folder {
@@ -43,51 +42,67 @@ impl Imap {
}
};
let foldername = folder.name();
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(foldername);
if folder_meaning == FolderMeaning::SentObjects {
// Always takes precedent
sentbox_folder = Some(folder.name().to_string());
} else if folder_meaning == FolderMeaning::Spam {
spam_folder = Some(folder.name().to_string());
} else if folder_name_meaning == FolderMeaning::SentObjects {
// only set iff none has been already set
if sentbox_folder.is_none() {
sentbox_folder = Some(folder.name().to_string());
}
} else if folder_name_meaning == FolderMeaning::Spam && spam_folder.is_none() {
spam_folder = Some(folder.name().to_string());
// Gmail labels are not folders and should be skipped. For example,
// emails appear in the inbox and under "All Mail" as soon as it is
// received. The code used to wrongly conclude that the email had
// already been moved and left it in the inbox.
let folder_name = folder.name();
if folder_name.starts_with("[Gmail]") {
continue;
}
if watched_folders.contains(&foldername.to_string()) {
info!(
context,
"Not scanning folder {} as it is watched anyway", foldername
);
} else {
info!(context, "Scanning folder: {}", foldername);
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
if let Err(e) = self.fetch_new_messages(context, foldername, false).await {
warn!(context, "Can't fetch new msgs in scanned folder: {:#}", e);
if let Some(config) = folder_meaning.to_config() {
// Always takes precedence
folder_configs.insert(config, folder.name().to_string());
} else if let Some(config) = folder_name_meaning.to_config() {
// only set if none has been already set
folder_configs
.entry(config)
.or_insert_with(|| folder.name().to_string());
}
let is_drafts = folder_meaning == FolderMeaning::Drafts
|| (folder_meaning == FolderMeaning::Unknown
&& folder_name_meaning == FolderMeaning::Drafts);
// Don't scan folders that are watched anyway
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
// Drain leftover unsolicited EXISTS messages
self.server_sent_unsolicited_exists(context);
loop {
self.fetch_new_messages(context, folder.name(), false)
.await
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !self.server_sent_unsolicited_exists(context) {
break;
}
}
}
}
context
.set_config(Config::ConfiguredSentboxFolder, sentbox_folder.as_deref())
.await?;
context
.set_config(Config::ConfiguredSpamFolder, spam_folder.as_deref())
.await?;
// We iterate over both folder meanings to make sure that if e.g. the "Sent" folder was deleted,
// `ConfiguredSentboxFolder` is set to `None`:
for config in &[
Config::ConfiguredSentboxFolder,
Config::ConfiguredSpamFolder,
] {
context
.set_config(*config, folder_configs.get(config).map(|s| s.as_str()))
.await?;
}
last_scan.replace(Instant::now());
Ok(())
}
}
async fn get_watched_folders(context: &Context) -> Vec<String> {
pub(crate) async fn get_watched_folders(context: &Context) -> Vec<String> {
let mut res = Vec::new();
let folder_watched_configured = &[
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),
@@ -95,8 +110,8 @@ async fn get_watched_folders(context: &Context) -> Vec<String> {
(Config::InboxWatch, Config::ConfiguredInboxFolder),
];
for (watched, configured) in folder_watched_configured {
if context.get_config_bool(*watched).await {
if let Some(folder) = context.get_config(*configured).await {
if context.get_config_bool(*watched).await.unwrap_or_default() {
if let Ok(Some(folder)) = context.get_config(*configured).await {
res.push(folder);
}
}

View File

@@ -26,7 +26,7 @@ impl Imap {
/// Issues a CLOSE command to expunge selected folder.
///
/// CLOSE is considerably faster than an EXPUNGE, see
/// https://tools.ietf.org/html/rfc3501#section-6.4.2
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
pub(super) async fn close_folder(&mut self, context: &Context) -> Result<()> {
if let Some(ref folder) = self.config.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);
@@ -37,7 +37,7 @@ impl Imap {
info!(context, "close/expunge succeeded");
}
Err(err) => {
self.trigger_reconnect();
self.trigger_reconnect(context).await;
return Err(Error::CloseExpungeFailed(err));
}
}
@@ -62,23 +62,23 @@ impl Imap {
/// select a folder, possibly update uid_validity and, if needed,
/// expunge the folder to remove delete-marked messages.
/// Returns whether a new folder was selected.
pub(super) async fn select_folder<S: AsRef<str>>(
pub(super) async fn select_folder(
&mut self,
context: &Context,
folder: Option<S>,
folder: Option<&str>,
) -> Result<NewlySelected> {
if self.session.is_none() {
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
self.trigger_reconnect();
self.trigger_reconnect(context).await;
return Err(Error::NoSession);
}
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(ref folder) = folder {
if let Some(folder) = folder {
if let Some(ref selected_folder) = self.config.selected_folder {
if folder.as_ref() == selected_folder {
if folder == selected_folder {
return Ok(NewlySelected::No);
}
}
@@ -88,31 +88,31 @@ impl Imap {
self.maybe_close_folder(context).await?;
// select new folder
if let Some(ref folder) = folder {
if let Some(folder) = folder {
if let Some(ref mut session) = &mut self.session {
let res = session.select(folder).await;
// https://tools.ietf.org/html/rfc3501#section-6.3.1
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
// says that if the server reports select failure we are in
// authenticated (not-select) state.
match res {
Ok(mailbox) => {
self.config.selected_folder = Some(folder.as_ref().to_string());
self.config.selected_folder = Some(folder.to_string());
self.config.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
}
Err(async_imap::error::Error::ConnectionLost) => {
self.trigger_reconnect();
self.trigger_reconnect(context).await;
self.config.selected_folder = None;
Err(Error::ConnectionLost)
}
Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.as_ref().to_string()))
Err(Error::BadFolderName(folder.to_string()))
}
Err(err) => {
self.config.selected_folder = None;
self.trigger_reconnect();
self.trigger_reconnect(context).await;
Err(Error::Other(err.to_string()))
}
}

Some files were not shown because too many files have changed in this diff Show More