Compare commits

...

342 Commits

Author SHA1 Message Date
jikstra
0f34ca8962 bump version to 1.84.0 2022-05-29 16:11:33 +00:00
link2xt
7def6e70ba mimeparser: explicitly handle decryption errors
mimeparser now handles try_decrypt() errors instead of simply logging
them. If try_decrypt() returns an error, a single message bubble
with an error is added to the chat.

The case when encrypted part is found in a non-standard MIME structure
is not treated as an encryption failure anymore. Instead, encrypted
MIME part is presented as a file to the user, so they can download the
part and decrypt it manually.

Because try_decrypt() errors are handled by mimeparser now,
try_decrypt() was fixed to avoid trying to load private_keyring if the
message is not encrypted. In tests the context receiving message
usually does not have self address configured, so loading private
keyring via Keyring::new_self() fails together with the try_decrypt().
2022-05-28 21:08:48 +00:00
jikstra
82c190a0c5 Node tests should not only run for pull requests 2022-05-27 08:18:50 +02:00
link2xt
ba74a40b6d Do not skip Sent and Spam folders on Gmail
Skipping of all [Gmail] folders was introduced to avoid scanning
virtual "[Gmail]/All Mail" folder. However, it also skips Sent and
Spam folders which reside inside [Gmail]. As a result
configured_sentbox_folder becomes unset after folder scan, making it
impossible to watch Sent folder, and Spam folder is never scanned.

This change makes Delta Chat identify virtual Gmail folders by their
flags, so virtual folders are skipped while Sent and Spam folders are
still scanned.
2022-05-21 13:04:53 +00:00
link2xt
64abe54b15 node: fix warnings about const 2022-05-24 23:54:36 +00:00
link2xt
ba0f5ee81d securejoin: replace thiserror with anyhow
Refactoring to reduce the number of custom error types.
2022-05-21 13:04:53 +00:00
missytake
eebd219414 Improve node publish ci (#3344)
* don't start workflow on py-*tag

* move node.js tests to separate action

* node.js CI: move PR ID and tags to environment variables

* node.js CI: small bracket mistake

* delete prebuilds.tar.gz before packaging

* use node/README.md as npm README
2022-05-24 00:41:18 +02:00
Hocuri
27fd0dbaec Update drafts/aeap-mvp.md (#3355)
* Rename aeap-mvp.rst to aeap-mvp.md

* Update aeap-mvp.md

* Apply suggestions from hpk's review

Co-authored-by: holger krekel  <holger@merlinux.eu>

* Additions to holger's review

* Decide on TODOs

* Drop the "If we are going to assign a message to a chat, but the sender is not a member of this chat" condition again

Co-authored-by: holger krekel  <holger@merlinux.eu>
2022-05-23 21:01:55 +02:00
Hocuri
b7af53c7d9 When changing self addr, transfer private key to new addr (#3351)
fix #3267
2022-05-23 19:25:53 +02:00
Hocuri
c762834e05 Make recv_msg() return the received Message (#3353) 2022-05-23 18:08:39 +02:00
link2xt
0725fe38f8 Disable clippy unwrap/expect lints again
They still fail on tests
2022-05-21 14:12:23 +00:00
link2xt
73341394ee Reduce unwrap and expect usage 2022-05-21 14:12:23 +00:00
Hocuri
9549aca48b Remove some AsRef<str> (#3354)
Using &str instead of AsRef is better for compile times, binary size and code complexity.
2022-05-23 12:57:50 +02:00
link2xt
9c41f0fdb9 Fix failure to decrypt first message to self after key change
The problem was in the handle_fingerprint_change() function which
attempted to add a warning about fingerprint change to all chats with
the contact.

This failed because of the SQL query failing to find the contact for
self in the `contacts` table. So the warning was not added, but at the
same time the whole decryption process failed.

The fix is to skip handle_fingerprint_change() for self addreses.
2022-05-23 10:09:29 +00:00
link2xt
1d522edb01 python: fix Chat.is_group() documentation
False is returned not only for 1:1 chat, but also in
other cases, such as mailing lists.
2022-05-23 11:56:56 +02:00
link2xt
b7d2828f60 Trim last newline in the chat encryption info
Android seems to display it, making the message box larger.
2022-05-21 17:03:48 +00:00
link2xt
deece15648 imap: do not unnecessarily SELECT folder in move_delete_messages()
If there are no MOVE/DELETE operations pending, the folder
should not be SELECTed.

When `only_fetch_mvbox` is enabled, `fetch_new_messages` skips
most folders, but `move_delete_messages` always selects the
folder even if there are no operations pending. Selecting
all folders wastes time during folder scan and turns
recent messages into non-recent.
2022-05-21 15:09:46 +00:00
link2xt
d06683489f Update to Rust 1.61.0 2022-05-21 14:28:55 +00:00
Jikstra
307063ade0 bump version to 1.83.0 (#3338) 2022-05-19 21:02:53 +02:00
Jikstra
ed00adbecc Fix node prebuilds path (#3337) 2022-05-19 20:56:34 +02:00
bjoern
2aaa850e25 prepare 1.82 (#3336)
* update changelog for 1.82.0

pr #3322 as added to 1.81.0 by accident;
it was never part of 1.81.0 but is now part of 1.82.0.

* bump version to 1.82.0
2022-05-19 19:45:13 +02:00
missytake
8859da42c6 Fix github action to publish node bindings (#3335)
* make upload to previews fail if it's executed on a tag not a PR

* node-package.yml: use tag in uploaded file name

* rename file to node-§tag.tar.gz before upload

* get rid of bash error?
2022-05-19 19:45:01 +02:00
Hocuri
2968c2919c Use params_iter() instead of manually constructing Vec 2022-05-18 19:13:28 +02:00
bjoern
33b10fa719 re-add removed DC_MSG_ID_MARKER1 as in use on iOS (#3330)
in fact, `get_chat_msgs()` with `marker1before` is not used on iOS,
however, iOS still needs the special chat-id.

in #3274 we did not check this possibility.

it may be changed, of course, however, that would requore some refactorings
in an anyway complicated area and is probably not worth the effort.
2022-05-17 19:31:11 +02:00
link2xt
6d189dab98 Fix race condition in alloc_ongoing()
Hold the same write lock while checking if ongoing
process is already allocated and while allocating it.
Otherwise it is possible for two parallel processes
running alloc_ongoing() to decide that no ongoing
process is allocated and allocate two ongoing processes.
2022-05-17 18:25:51 +02:00
bjoern
380d7e66b5 prepare 1.81 (#3329)
* update changelog for 1.81.0

* bump version to 1.81.0
2022-05-17 17:07:47 +02:00
bjoern
eb80fa4eb0 create same contact-colors when addresses differ in upper-/lowercase (#3327)
* create same contact-colors when addresses differ in upper-/lowercase

this leaves group-colors based on group names as is,
so, "MY GROUP" creates a different color than "my group",
as these names are better synced and also not an ID in this sense,
this is probably fine here.
(also when looking at the examples from
https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd ,
case-sensistifity for group names seems to be fine)

* add a test for upper-/lowercase in group names

* update CHANGELOG
2022-05-17 14:48:33 +02:00
Hocuri
88b470e8e5 Better error reporting when creating a folder fails (#3325)
Fixes #3310
2022-05-17 10:40:44 +02:00
bjoern
99f8785475 unify language and method names (#3321) 2022-05-16 22:41:42 +02:00
link2xt
47db1349a9 securejoin: remove unused error types related to ongoing processes
securejoin no longer allocates ongoing processes
2022-05-16 21:50:45 +02:00
Simon Laux
1f8b04bd42 add webxdc document property to deltachat node typings 2022-05-16 14:41:55 +02:00
link2xt
d790a5fba9 Update link to K-9 Mail blog post 2022-05-15 23:38:27 +00:00
bjoern
2fc0a0964b allow webxdc document names (#3317)
* allow webxdc document names

* test document webxdc property

* update CHANGELOG
2022-05-15 12:10:09 +02:00
link2xt
715664273b Repair encrypted mails turned into attachments
Google Workspace has an option "Append footer" which appends standard
footer defined by administrator to all outgoing messages. However,
there is no plain text part in encrypted messages sent by Delta Chat,
so Google Workspace turn the message into multipart/mixed MIME, where
the first part is an empty plaintext part with a footer and the second
part is the original encrypted message.

This commit makes Delta Chat attempt to repair such messages,
similarly to how it already repairs "Mixed Up" MIME structure in
`get_mixed_up_mime`.
2022-05-14 15:18:51 +00:00
B. Petersen
a9f707c398 document source_code_url in webxdc-reference 2022-05-14 14:02:17 +02:00
link2xt
8168bcece5 Improve Chat.get_encryption_info() format
Group contacts by peerstate and make it easier
to find contacts that prevent encryption by sorting them
to the top of the list.
2022-05-14 14:01:46 +02:00
missytake
c9beaa2aa1 finish integrating node bindings into core repository
don't ignore core sourcefiles,
prevented npm installation on architectures with no prebuild
don't run lint checks on windows
github actions don't like double quotes apparently
minimize node.js CI
update ubuntu runner to 22.04
README: update link to node bindings source
simplify link in readme
node: fix crash with invalid account id
(throw error when getContext failed)
fix typo in readme
remove node specific changelog
change prebuild machine back to ubuntu 18.04
move package.json to root level to include rust source in npm package
change path in m1 patch
github action to upload to download.delta.chat/node/ on tag
try build with ubuntu 20.04
Update node/README.md
try building it with newer ubuntu because it wants glibc 2.33
fix path for prebuildify script
throw error when instanciating a wrapper class on `null`
(Context, Message, Chat, ChatList and so on)
try fix selecting the right cache
to fix the strange glibc bug
also revert back ubuntu version to 18.04
also bump package.json version with release script
fix paths since we moved around package.json
github action: fix path
document npm release - it's so much easier now!
there are no PR checks to post to if this action is executed on a tag
github action: fix artifact names
fix paths? wtf do I know, it's 3AM and I'm drunk
fix syntax error
don't upload preview if action is run on tag
is the tag ID an empty string or null?
node-package github action is done so far
also include scripts in package
only publish docs on push to master branch
actually bump package.json version in set_core_version script
prettify package.json
fix test - we don't really need to assert that
remove unnecessary ls statement from github action
2022-05-13 21:25:35 +02:00
missytake
b238c7e743 pull latest changes from deltachat-node 2022-05-13 21:25:35 +02:00
missytake
e9511ebfc3 first steps to integrate deltachat-node to core repository, adjust CI:
adjust scripts to new location of deltachat-core-rust
adjust dc-node readme to repo change
mention old repository
migrate github actions, try out if they work
fix path to node docs in SSH github action
passing mailadm token to node tests
hopefully fixing the download paths for the artifacts
fixing download paths, this time for real
post upload link to details
fix scp command
forgot to remove platform_status dict
fixing paths in the github action
add github action to delete node preview builds when PR is closed
move environment variable to yaml
remove git trash
github action to release to npm
use different action which also works with branches for testing
we don't want to publish to NPM through the CI
see what lint issues windows has
2022-05-13 21:25:35 +02:00
missytake
a786a1427d blindly copying deltachat-node to core repository 2022-05-13 21:25:35 +02:00
bjoern
961612370d add source_code_url to manifest and get_webxdc_info, add a test (#3314) 2022-05-13 21:00:36 +02:00
link2xt
bd5b9573f6 Deprecate marker1before argument of dc_get_chat_msgs() 2022-05-13 20:55:43 +02:00
bjoern
e603a10ab4 webxdc: discourage usage of the <title> tag (#3309) 2022-05-11 22:07:43 +02:00
Rosano
984346d682 Link to guidebook 2022-05-11 18:35:54 +02:00
Rosano
f4cecb61bc Link to webxdc organization 2022-05-10 22:52:31 +02:00
holger krekel
cffa101419 remove usage of py in favor of pathlib 2022-05-09 21:22:19 +02:00
holger krekel
1d7d201b5b remove a log line and a return 2022-05-09 21:22:19 +02:00
holger krekel
c0e4e12138 Introduce caching of configured live accounts in each test process 2022-05-09 21:22:19 +02:00
link2xt
5a85255be9 Reduce number of generic impl AsRef 2022-05-08 21:21:40 +00:00
link2xt
60d3960f3a scheduler: make Scheduler stateless
Scheduler has no Stopped state anymore. If Scheduler exists, it is
always started. Scheduler is stopped via Scheduler.stop(), which
consumes Scheduler and cannot fail.
2022-05-08 18:53:15 +00:00
link2xt
7bcb03f1ec Replace SendMdn job with smtp_mdns table
Unlike jobs which are executed before sending normal messages, MDNs
from `smtp_mdns` table are sent after sending messages from `smtp`
table. This way normal messages have higher priority than MDNs.

There are no SMTP jobs anymore. All jobs are IMAP jobs, so
`jobs.thread` column is not used anymore.
2022-05-08 18:33:41 +00:00
bjoern
01ef053a11 bubble up signing error instead of panicing (#3304)
.expect() may panic, which is probably not what we want here.
it seems better to bubble up the error (as we are doing in the other cases)

(i was checking some .expect usages after we had a similar issue at #3264)
2022-05-08 20:17:50 +02:00
Hocuri
8988c775fe Abort instead of unwinding on panic (#3259) 2022-05-08 17:52:52 +02:00
holger krekel
c8bfa98b6b actually enable online account caching. previously it was creating (>100) online test accounts 2022-05-05 16:18:59 +02:00
bjoern
34053c7608 prepare 1.80 (#3288)
* update changelog for 1.80.0

* bump version to 1.80.0
2022-05-05 12:21:12 +02:00
bjoern
785667ec07 fix qr-code display (#3295)
* tagger.put_raw() has changes sematics and escapes strings on its own now

an explicit escaping leads to double escaping and to wrong display.

this should also improve lenght calculation,
as a quote and other specials counts as 1 character and not as 4-6.

* test encoding of generated qr-code-svg

* streamline function argument wording

use qrcode_description instead of raw_qrcode_description -
there is nothing "raw" in the argument,
it is a string as used throughout the app.
2022-05-05 11:55:05 +02:00
bjoern
9a0a3c4b00 fix "scheduler already started" log entry (#3294)
i was wondering mainly about the whole line
"Failed to start IO: scheduler is already stopped".
i think it has to be "already started" and not "already stopped".
2022-05-04 18:09:55 +02:00
missytake
e56c261c73 Merge pull request #3292 from deltachat/fix_more_py
fix flaky test to not rely on timing
2022-05-04 13:34:48 +02:00
holger krekel
32a9db6922 fix flaky test to not rely on timing 2022-05-04 13:29:08 +02:00
holger krekel
77498c17a5 reorder tests to provide a complex/offline/online distinction, also speeding up distributed test runs. 2022-05-04 13:17:08 +02:00
holger krekel
54c07f89f2 also guard the ac_process_ffi_event calling 2022-05-04 12:08:47 +02:00
holger krekel
cd4d265055 safer handling of calling account hooks, refined shutdown comment 2022-05-04 12:08:47 +02:00
holger krekel
81ee69010d add a comment, fix a typo in debug message 2022-05-04 12:08:47 +02:00
holger krekel
138d5b7a02 fix segfaults with python runs -- i don't get them anymore, and can also control-c at will 2022-05-04 12:08:47 +02:00
bjoern
95a54a43ff update provider database (#3284)
ran `./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs`
2022-05-03 23:57:53 +02:00
holger krekel
ca59cbc898 make direct_imap idle usage safe from tests, so we never miss finishing direct_imap idle 2022-05-03 22:56:23 +02:00
holger krekel
21f72ad845 new --debug-setup option to show events during account setup. 2022-05-03 22:50:16 +02:00
holger krekel
40fd98d580 show logs for pseudo configured accounts (for online ones they are instantiated later) 2022-05-03 22:50:16 +02:00
holger krekel
0422d751d8 avoid instatiating a full blown Account object just for showing some core info 2022-05-03 19:35:03 +02:00
holger krekel
e44f68db83 prevent one strange traceback probably caused by ongoing shutdown 2022-05-03 19:35:03 +02:00
holger krekel
88cbf42c39 fix various tests 2022-05-03 19:35:03 +02:00
holger krekel
45157f79a3 fix examples 2022-05-03 19:35:03 +02:00
holger krekel
f5157392b6 some renaming and some docstrings 2022-05-03 19:35:03 +02:00
holger krekel
f631ec3a7c shift startio/init machinery into PendingConfigure class 2022-05-03 19:35:03 +02:00
holger krekel
a9b2750ec9 slight renamings 2022-05-03 19:35:03 +02:00
holger krekel
7fbdb42695 remove _configtracker and write a tested "PendingConfigure" helper class for managing configure/started states for test accounts 2022-05-03 19:35:03 +02:00
holger krekel
026dd4480e add --extra-info CLI option (defaults to False for interactive runs) 2022-05-03 19:35:03 +02:00
holger krekel
b373e7acba more renames 2022-05-03 19:35:03 +02:00
holger krekel
d2ca54c167 provide for meaningful names for bringing accounts online for test functions 2022-05-03 19:35:03 +02:00
holger krekel
e070284a09 strike setting "displayname" in test plugin and refine two tests which implicitely relied on this 2022-05-03 19:35:03 +02:00
holger krekel
e4e02ea3c0 improve error messages during live creation of accounts 2022-05-03 19:35:03 +02:00
holger krekel
a947980eb6 slight reorg for creating accounts from liveconfig 2022-05-03 19:35:03 +02:00
holger krekel
f11c3dd3e3 fix bench test 2022-05-03 19:35:03 +02:00
holger krekel
394067be63 refactor session_liveconfig_producer to become a more geneal testprocess management object 2022-05-03 19:35:03 +02:00
holger krekel
2494613583 - perform direct_imap init in testplugin instead of global deltachat
plugin, probably also helping to avoid some segfeaults during teardown

- some API renaming on the side (too hard to split into separate commit, sorry)
2022-05-03 19:35:03 +02:00
holger krekel
77c60e7450 refine test support function name 2022-05-03 19:35:03 +02:00
holger krekel
04dd2d93d0 trim online account creation to a single get_online_accounts() function 2022-05-03 19:35:03 +02:00
holger krekel
5e5710ecce streamline configuration handling for test accounts, removing one layer of flags 2022-05-03 19:35:03 +02:00
holger krekel
c1b33a66c4 refactor "quiet" parameter away and provide more precise logging-control 2022-05-03 19:35:03 +02:00
holger krekel
0e3165d724 unify get_unconfigured_account and make_account 2022-05-03 19:35:03 +02:00
holger krekel
3e12eab0b5 remove make_account from get_online_config and rename the latter to get_next_liveconfig to avoid one indirection 2022-05-03 19:35:03 +02:00
holger krekel
87365e4a43 - remove superflous early set_configs to separate config / account making better
- avoid low-level dc_* API access from testplugin
2022-05-03 19:35:03 +02:00
holger krekel
3e16a47ff2 remove unnccesary distinction between offline/online accounts in make_account, simplifying api/usage 2022-05-03 19:35:03 +02:00
holger krekel
874054148e streamline session_live_config implementation and usage 2022-05-03 19:35:03 +02:00
holger krekel
5e39a13bf6 refine waiting for initial startup waiting for "INBOX: Idle" ready
this slows down initialization for tests but provides more stability in my runs
2022-05-03 19:35:03 +02:00
holger krekel
720c1b5eca normalize make_account usage 2022-05-03 19:35:03 +02:00
holger krekel
c7c1a04c6a move large inlined AccountMaker (renamed to ACFactory) to proper class instead of being defined in closure 2022-05-03 19:35:03 +02:00
holger krekel
fd5b224ba0 simplify test setup api by removing pre_generated_keys arguments 2022-05-03 19:35:03 +02:00
holger krekel
0b80ade3ae fix "retry" wording, it isn't but a try :) 2022-05-03 09:31:38 +02:00
bjoern
e1c3e95307 prepare 1.79 (#3281)
* update changelog for 1.79.0

* bump version to 1.79.0
2022-05-02 16:18:00 +02:00
link2xt
904e8966c0 Replace location jobs with async location loop
Locations are now sent in the background regardless
of whether SMTP loop is interrupted or not.
2022-05-01 23:08:34 +00:00
link2xt
3a10f0155f Remove panics from the scheduler and simplify start/stop_io()
Hold scheduler lock during the whole procedure of scheduler starting
and stopping. This ensures that two processes can't get two read locks
in parallel and start loops or send the stop signal twice.

Also remove shutdown channels: it is enough to wait
for the loop handle without receiving a shutdown signal
from the end of the loop.
2022-04-30 18:08:01 +00:00
Hocuri
4c9cc4f3d4 Hopefully make test_connectivity() less flaky 2022-04-30 18:07:39 +02:00
link2xt
48f2c4e14b Correctly escape messages consisting of a dot in SMTP protocol
Actual bugfix is in the async-smtp crate.
2022-04-30 13:30:31 +00:00
holger krekel
f41df327a9 add a little bench file -- "pytest tests/bench_empty.py --durations=10" will tell you how much overhead there is 2022-04-30 13:27:02 +02:00
holger krekel
3f9e3038b7 strike last hard-coded ref to configured_addr 2022-04-30 13:26:50 +02:00
bjoern
c75c95afa9 prepare 1.78 (#3261)
* update changelog for 1.78.0

* bump version to 1.78.0
2022-04-29 18:01:55 +02:00
missytake
d4e0009b89 Merge pull request #3260 from deltachat/imap-tools
replaced imapclient python library with imap-tools
2022-04-29 17:58:50 +02:00
missytake
b97b374487 move imap_tools mypy ignore to mypy.ini 2022-04-29 16:01:48 +02:00
missytake
e27345e489 python bindings: ignore mypy errors for imap_tools 2022-04-29 15:19:48 +02:00
missytake
032e644b2b set default timeout to None 2022-04-29 11:30:06 +02:00
holger krekel
b7ac81701a update depss before we have a few stable core releases 2022-04-29 11:20:25 +02:00
missytake
d59aa35b2f fix mypy errors 2022-04-29 11:14:19 +02:00
holger krekel
4c7c4e2a81 better document one sometimes failing test 2022-04-29 10:06:02 +02:00
holger krekel
521fa58b75 remove timeout 2022-04-29 10:00:43 +02:00
holger krekel
a2e5c60683 - remove one unncessary usage of imap idle
- simplify SEEN bytes/unicode flag issue
- fix a lint issue and a docstring
2022-04-29 09:42:05 +02:00
missytake
5ef152fd84 replaced imapclient python library with imap-tools in the tests. works with testrun.org locally 2022-04-28 16:50:36 +02:00
bjoern
e2ba338923 remove network from dc_provider_new_from_email(), add an explicit function for network provider lookup (#3256) 2022-04-27 15:51:40 +02:00
link2xt
aae4f0bb7b Trash location.kml messages
Assign location.kml message parts to the trash chat,
but return non-trash chat_id so locations are assigned
to the correct chat.

Due to a bug introduced in
7968f55191
previously location.kml messages resulted in
empty message bubbles on the receiver.
2022-04-24 19:43:59 +00:00
Robert Schütz
43e3f8f08b python: use pkg-config for system install 2022-04-26 21:48:53 +02:00
bjoern
9cc2fd555f resend messages using the same Message-ID (#3238)
* add dc_resend_msgs() to ffi

* add 'resend' to repl

* implement resend_msgs()

* allow only resending if allowed by chat-protection

this means, resending is denied if a chat is protected and we cannot encrypt
(normally, however, we should not arrive in that state)

* allow only resending of normal, non-info-messages

* allow only resending of own messages

* reset sending state to OutPending on resending

the resulting state is always OutDelivered first,
OutMdnRcvd again would be applied when a read receipt is received.

preserving old state is doable, however,
maybe this simple approach is also good enough, at least for now
(or maybe the simple approach is even just fine :)

another thing: when we upgrade to resending foreign messages,
we do not have a simple way to mark them as pending
as incoming message just do not have such a state -
but this is sth. for the future.
2022-04-26 20:59:17 +02:00
Hocuri
c10dc7b25b re-add quotes in SEARCH command, comment 2022-04-26 18:56:35 +02:00
Hocuri
9e1770316a Use plain get_config(Config::ConfiguredAddr) to not ignore db errors 2022-04-26 18:56:35 +02:00
Hocuri
0e595c9801 Keep the self address casing again instead of lowercasing it 2022-04-26 18:56:35 +02:00
Hocuri
6ae9e43183 schedule_resync() instead of deleting imap_sync 2022-04-26 18:56:35 +02:00
Hocuri
18126b42cb Gossip to secondary addrs in group again 2022-04-26 18:56:35 +02:00
Hocuri
2b233fd810 Don't let repeat_vars() return unnecessary Result 2022-04-26 18:56:35 +02:00
Hocuri
a4f5d2b9b2 More functional get_all_self_addrs() 2022-04-26 18:56:35 +02:00
Hocuri
d29c09caf3 Make sure that the server UIDs are reset when changing accounts 2022-04-26 18:56:35 +02:00
Hocuri
bc809986e7 If unconfigured, let get_all_self_addrs() return vec![], not vec![""]; 2022-04-26 18:56:35 +02:00
Hocuri
5ee2f3696d Fix todo: Make get_primary_self_addr() always lowercase the result 2022-04-26 18:56:35 +02:00
Hocuri
df5eb546e7 Bring back the check that the contact's addr is no self addr in get_all()
Create params_iter() and params_iterv![] helper functions
2022-04-26 18:56:35 +02:00
holger krekel
684351c753 properly construct imap search command for multiple self addresses 2022-04-26 18:56:35 +02:00
holger krekel
a8342e37b9 address latest review comments, move test to implementation file 2022-04-26 18:56:35 +02:00
Hocuri
3b6fc9959f Introduce SecondaryAddrs config and make stuff work 2022-04-26 18:56:35 +02:00
holger krekel
3ffc985968 try fix a sometimes failing test: don't test python's imap idle as it's not needed. also add some more logging. 2022-04-26 15:28:24 +02:00
holger krekel
369609b26c streamline emitting MsgsChanged and IncomingMsg event to go through particular functions. 2022-04-26 10:09:21 +02:00
link2xt
d033dcf395 Run python tests in a single thread
Work around mailcow rate limits
2022-04-25 15:43:47 +02:00
link2xt
5ef2a85c10 python: do not crash in get_locations() when location has no marker 2022-04-25 15:05:34 +02:00
link2xt
8c0bc9080c Create "Junk" folder in test_dont_show_emails()
This folder may not exist on the test server.
2022-04-24 00:20:41 +00:00
link2xt
6bf2c5415f Don't count IMAP jobs in scheduler
The limit on the number of jobs executed in a row was introduced to
prevent large queues of small jobs like MarkseenMsgOnImap, MoveMsg and
DeleteMsgOnImap from delaying message fetching. Since all these jobs
are now removed and IMAP operations they did are now batched, it is
impossible to have 20 or more queued IMAP jobs.
2022-04-23 19:53:10 +00:00
link2xt
2f31033a88 Ignore messages from all spam folders if there are many
For example, if there is both a Spam and Junk folder,
both of them should be ignored, even though only one
of them can be a ConfiguredSpamFolder.
2022-04-23 19:23:31 +00:00
Hocuri
ceaed0f552 Speedup rust tests (#3242)
Speeds up running the rust tests by about a factor of 2.
2022-04-23 13:50:58 +02:00
Hocuri
e9963ecc0d Also run clippy for benchmarks in CI (#3241)
…and fix two lints
2022-04-22 15:06:34 +02:00
link2xt
9ef0b43c36 Remove job::{Action,Thread}::Unknown variants 2022-04-17 00:00:00 +00:00
holger krekel
801d636eb5 enhance waiting for direct imap idle events to prevent randomness 2022-04-20 23:32:45 +02:00
link2xt
dfbfd4fe74 Remove housekeeping job
Run housekeeping directly from the inbox loop
instead of creating inbox loop job.
2022-04-17 00:00:00 +00:00
bjoern
7455989729 webxdc documentation: make function declaration correct JavaScript (#3237)
* webxdc docs: make function declaration correct js

* add 'let'
2022-04-20 12:16:40 +02:00
Sebastian Klähn
8b2b9e1093 update dev-reference to reflect new promise feature for setup (#3220)
* update dev-reference

* Update draft/webxdc-dev-reference.md

Co-authored-by: holger krekel  <holger@merlinux.eu>

* Update draft/webxdc-dev-reference.md

Co-authored-by: Simon Laux <Simon-Laux@users.noreply.github.com>
Co-authored-by: holger krekel  <holger@merlinux.eu>
2022-04-19 15:43:05 +02:00
link2xt
a8cf05ea5d accounts: retry remove_account multiple times on failure
When removing an account, try 60 times with 1 second sleep in between
in case removal of database files fails. This happens on Windows
platform sometimes due to a known bug in r2d2 which may result in 30
seconds delay until all connections are closed [1].

[1] https://github.com/sfackler/r2d2/issues/99
2022-04-19 14:14:38 +02:00
Robert Schütz
969508ae36 dynamic libraries use dylib extension on Darwin 2022-04-19 08:56:29 +02:00
bjoern
f581ecc805 url-safe id generation (#3231)
* test dc_create_id() for invalid characters

* create url-save ids (as documented)
2022-04-18 17:21:33 +02:00
link2xt
a63464765c dc_receive_imf: remove Received: based draft detection heuristic
Proper draft detection was implemented in
bf7f64d50b

Removing this heuristic also removes the need to pass IMAP folder name
around.
2022-04-17 00:00:00 +00:00
link2xt
e9fe8ce118 Merge branch 'markseen-imap-loop' 2022-04-17 17:55:25 +03:00
link2xt
1fa892c239 Rename store_seen_flags into store_seen_flags_on_imap 2022-04-17 14:55:10 +00:00
link2xt
1afbbbc737 Rename markseen_on_imap to markseen_on_imap_table and document it 2022-04-17 11:56:48 +00:00
link2xt
66c4de2607 Mark messages as seen in IMAP loop
MarkseenMsgOnImap job, that was responsible for marking messages as
seen on IMAP and sending MDNs, has been removed.

Messages waiting to be marked as seen are now stored in a
single-column imap_markseen table consisting of foreign keys pointing
to corresponding imap table records.

Messages are marked as seen in batches in the inbox loop. UIDs are
grouped by folders to reduce the number of requests, including folder
selection requests. UID grouping logic has been factored out of
move_delete_messages into UidGrouper iterator to avoid code duplication.

Messages are marked as seen right before fetching from the inbox
folder. This ensures that even if new messages arrive into inbox while
the connection has another folder selected to mark messages there, all
messages are fetched before going IDLE. Ideally marking messages as
seen should be done after fetching and moving, as it is a low-priority
task, but this requires skipping IDLE if UIDNEXT has advanced since
previous time inbox has been selected. This is outside of the scope of
this change.

MDNs are now queued independently of marking the messages as seen.
SendMdn job is created directly rather than after marking the message
as seen on IMAP. Previously sending MDNs was done in MarkseenMsgOnImap
avoid duplicate MDN sending by setting $MDNSent flag together with
\Seen flag and skipping MDN sending if the flag is already set. This
is not the case anymore as $MDNSent flag support has been removed in
9c077c98cd and duplicate MDN sending in
multi-device case is avoided by synchronizing Seen status since
833e5f46cc as long as the server
supports CONDSTORE extension.
2022-04-17 09:50:04 +00:00
link2xt
213e67dea2 Merge branch 'faster_fetch' 2022-04-17 09:47:42 +00:00
link2xt
86c884fe1e Update the comment before delete_expired_imap_messages 2022-04-17 09:47:17 +00:00
bjoern
a4d5c8cd2f show 'Not connected' if storage information are not yet available (#3222)
* show 'Not connected' if storage information are not yet available

'One moment' is a bit misleading in case the device is offline
as it will take more than a moment until that will be updated :)

* update CHANGELOG
2022-04-16 18:35:25 +02:00
Simon Laux
330665afbe don't start io on unconfigured context 2022-04-16 17:48:16 +02:00
holger krekel
be1d87c3c3 do imap expiration/deletion after fetching messages, to avoid unneccessary delays. 2022-04-16 17:19:31 +02:00
link2xt
14ab3c8651 Remove unused stop-token dependency
stop-token 0.2.0 is required by async-imap. This Cargo.toml entry
additionally pulled in unused stop-token 0.7.0.
2022-04-16 11:24:23 +00:00
holger krekel
9c04ed483e Streamline access/working with configured params and configured addr (#3219) 2022-04-16 09:50:26 +02:00
Hocuri
a8a5e184ab Don't unnecessarily interrupt ephemeral loop (#3221)
Follow-up for #3211
2022-04-15 21:30:01 +02:00
Sebastian Klähn
c571595980 Fix #3186 (#3218)
* fix

* code-review fixes

* check if chat is restored correctly

* add changelog-entry

* Update src/dc_receive_imf.rs

Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>

* Update CHANGELOG.md

Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>

Co-authored-by: Asiel Díaz Benítez <adbenitez@nauta.cu>
2022-04-14 19:12:31 +02:00
Sebastian Klähn
80f2ccc9ed improve error message (#3217)
use actual path instead of placeholder
2022-04-14 12:32:44 +02:00
bjoern
b2e9e57859 add status_update_serial to webxdc_status_update event (#3215)
this bypasses the replication safety introduced by letting the caller track the last serial,
however, in case of bots that do not track much state and do not playback updates anyway,
it is still useful.
2022-04-14 11:12:17 +02:00
Hocuri
3c75b36148 clippy 2022-04-12 22:26:44 +00:00
Hocuri
bcd8e330cb Add test for ephemeral deletion 2022-04-12 22:26:44 +00:00
link2xt
f75f8ad76d Interrupt ephemeral loop when new messages are fetched 2022-04-12 22:26:44 +00:00
link2xt
574b78cf31 Interrupt ephemeral loop when delete_device_after is set 2022-04-12 22:26:44 +00:00
link2xt
92f0e8472b Take delete_device_after into account when calculating ephemeral loop timeout 2022-04-12 22:26:44 +00:00
bjoern
69bd5d2ab0 Webxdc scaling/viewport is implementation specific (#3214)
at least on iOS we would need to inject a script to force a behavior,
that is hard to merge with settings set by the Webxdc.

therefore, the easiest approach seems to be to leave that completely up to the Webxdc -
and, in practise, many Webxdc already set viewports, including scaling.
2022-04-12 14:54:05 +02:00
Floris Bruynooghe
6eaf04107d Auto-approve dependabot PRs
There is very little to be gained by having to approve those PRs, and
it is a lot of UI interaction to get them approved.
They still will need to be merged manually regardless, so they might
as well be approved by a bot.
2022-04-12 12:44:04 +02:00
bjoern
d80fdb20c7 make Connectivity-View-HTML not scalable (#3213)
* make Connectivity-View-HTML not scalable

in practise, Android and Desktop already disallow scaling
by some other methods,
however, for iOS this is needed as we otherwise
have to do far more complicated things as drafted at
https://github.com/deltachat/deltachat-ios/pull/1531/files

* update CHANGELOG
2022-04-12 12:26:40 +02:00
Hocuri
f618c87ee5 Follow http redirects for autoconfigure (#3208) 2022-04-10 19:27:05 +02:00
bjoern
0721c22073 prepare 1.77 (#3209)
* update changelog for 1.77.0

* bump version to 1.77.0

* Update CHANGELOG.md

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

* reorder changelog, adapt writing style

Co-authored-by: Hocuri <hocuri@gmx.de>
2022-04-10 17:01:30 +02:00
holger krekel
aba066b4d1 only do monthly dependabot updates (#3199)
* only do monthly dependabot updates

* increase dependabot pr limit from 10 to 50 (due to only monthly interval, 10 seems to be too low)

Co-authored-by: B. Petersen <r10s@b44t.com>
2022-04-10 12:38:27 +02:00
Hocuri
2562c726e6 Do ephemeral deletion in async task background loop (#3194)
* Do ephemeral deletion in background loop

1. in start_io start ephemeral async task, in stop_io cancel ephemeral async task

2. start ephemeral async task which loops like this:

- wait until next time a message deletion is needed or an interrupt occurs (see 3.)
- perform delete_expired_messages including sending MSGS_CHANGED events

3. on new messages (incoming or outgoing) with ephemeral timer:

- interrupt ephemeral async task

* Changelog

* Fix and improve test

* no return value needed

* address @link2xt review comments

* slight normalization: have only one place where we wait for interrupt_receiver

* simplify sql statement -- and don't exit the ephemeral_task if there is an sql problem but rather wait

* Remove now-unused `ephemeral_task` JoinHandle

The JoinHandle is now inside the Scheduler.

* fix clippy

* Revert accidental move of the line

* Add log

Co-authored-by: holger krekel <holger@merlinux.eu>
Co-authored-by: link2xt <link2xt@testrun.org>
2022-04-10 12:22:47 +02:00
bjoern
6e3ec71c10 show an error when a webxdc is written for a newer (future) api (#3206)
* add min_api to manifest.toml, define WEBXDC_API_VERSION

* return an error instead of index.html if the webxdc requires a newer api

* add a test with an webxdc requiring a newer api

* update CHANGELOG
2022-04-10 12:16:28 +02:00
Hocuri
2932c1ed35 Configure: Try "imap.*"/"smtp.*"/"mail.*" first (#3207)
* Configure: Try "imap.*"/"smtp.*"/"mail.*" first

Fix half of #3158.

Try "imap.ex.org"/"smtp.ex.org" and "mail.ex.org" first because if a server exists
under this address, it's likely the correct one.

Try "ex.org" last because if it's wrong and the server is configured to
not answer at all, configuration may be stuck for several minutes.

* Changelog

* Add test
2022-04-10 12:16:00 +02:00
Asiel Díaz Benítez
149b31a960 Merge pull request #3187 from deltachat/adb/issue-2557
Send setup-changed messages only in the chats we share with the peer
2022-04-09 19:43:51 -04:00
adbenitez
f1d09e4127 apply rustfmt 2022-04-09 19:15:48 -04:00
Asiel Díaz Benítez
10bdbc95cd Update src/peerstate.rs
Co-authored-by: bjoern <r10s@b44t.com>
2022-04-09 18:23:14 -04:00
link2xt
26c38070ec Disable unused async-smtp transports
By default file and sendmail transports are enabled,
but deltachat does not use them.
2022-04-09 11:36:32 +00:00
link2xt
494a7f1db9 Update to Rust 1.60
It re-enables incremental compilation.
2022-04-09 09:10:51 +00:00
holger krekel
bba721654b update deps for 1.30 release series 2022-04-08 13:08:19 +02:00
Hocuri
36a17b0592 oops 2022-04-08 10:54:45 +02:00
Hocuri
b7294d46cf Changelog 2022-04-08 10:54:45 +02:00
Hocuri
d8977b5046 Drop unused table backup_blobs in migration
Today, we solved an issue with holger's phone that he couldn't export
his account anymore because during the `VACUUM` the Android system
killed the app because of OOM. The solution was to drop the table
`backup_blobs`, so let's automatically do this in migration

This table was used back in the olden days when we backuped by exporting
the dbfile and then putting all blobs into it. During import, the
`backup_blobs` table should have been dropped, seems like this didn't
work here.
2022-04-08 10:54:45 +02:00
dependabot[bot]
963bb7f7cf cargo: bump syn from 1.0.90 to 1.0.91
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.90 to 1.0.91.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.90...1.0.91)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-07 11:41:57 +02:00
Hocuri
7d3a08599e Changelog 2022-04-06 17:51:01 +02:00
Hocuri
345a4bc504 Give setup-changed messages the same timestamp as the previous message (#3188) 2022-04-06 17:05:44 +02:00
dependabot[bot]
7bfdf2e2f5 Merge pull request #3189 from deltachat/dependabot/cargo/zip-0.6.2 2022-04-05 08:27:49 +00:00
dependabot[bot]
d9ac5d88e9 cargo: bump zip from 0.6.1 to 0.6.2
Bumps [zip](https://github.com/zip-rs/zip) from 0.6.1 to 0.6.2.
- [Release notes](https://github.com/zip-rs/zip/releases)
- [Commits](https://github.com/zip-rs/zip/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-04 21:13:27 +00:00
bjoern
2cf11bb2ea muted chats stay archived (#3184)
* do not unarchive muted chats on sending/receving messages

* add a test for unarchive muted chats

* update CHANGELOG

* improve test_unarchive_if_muted
2022-04-04 11:02:36 +02:00
adbenitez
e29d008914 send setup-changed messages only in the chats we share with the peer, do not create contact request 2022-04-04 01:05:49 -04:00
link2xt
de91063fbe scheduler: add comment about fake-idle timeout 2022-04-03 19:30:56 +00:00
link2xt
3d95272707 smtp: retry message sending automatically if loop is not interrupted 2022-04-03 18:55:29 +00:00
Floris Bruynooghe
3c20d0902e Add explicit tests for special ContactId values
Turns out some FFI users don't use the constants.  Scary.
2022-04-03 20:35:09 +02:00
Floris Bruynooghe
f2c1e5c6e5 Replace some ContactId::new() calls with constants 2022-04-03 20:35:09 +02:00
Floris Bruynooghe
feb354725a Make ContactId::LAST_SPECIAL private
Also remove the Ord implementations, this makes ContactId more opaque
by no longer letting people deal with the fact this is ordered.
2022-04-03 20:35:09 +02:00
Floris Bruynooghe
35c0434dc7 Move ContactId constants to struct.
This makes the APIs much more Rust-like and keep contact IDs clearer
and in one place.
2022-04-03 20:35:09 +02:00
Hocuri
918ee47c79 Consider outgoing messages to just one receiver as "private message" (#3177) 2022-04-03 19:19:10 +02:00
link2xt
a8ab7c9c04 smtp: do not try to use stale connections
Previously first try used an old connection and. If using stale
connection, an immediate retry was performed using a new connection.

Now if connection is stale, it is closed immediately without trying to
use it.

This should reduce the delay in cases when old connection is unusable.
2022-04-03 13:11:27 +00:00
link2xt
332892b468 ephemeral: clear more fields in delete_expired_messages
These fields are cleared in other places,
but in the case of delete_device_after setting
only txt column was cleared previously.
2022-04-02 17:10:09 +00:00
link2xt
6392300311 job: remove unnecessary anyhow::Error import 2022-04-02 17:06:22 +00:00
Floris Bruynooghe
16d201faca Re-enable custom Display for ContactId
Caught another case of using Display instead of ToSql, now all tests
pass again.
2022-04-02 17:19:00 +02:00
dependabot[bot]
ea0cf67f98 cargo: bump zip from 0.6.0 to 0.6.1
Bumps [zip](https://github.com/zip-rs/zip) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/zip-rs/zip/releases)
- [Commits](https://github.com/zip-rs/zip/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-02 16:09:44 +02:00
holger krekel
612132b7c8 move invariant out of loop, less LOC and 1.5% faster 2022-04-01 14:33:57 +02:00
holger krekel
26470c6047 apply hocuri's niceifcation 2022-04-01 14:33:57 +02:00
holger krekel
93d3522f67 stylistic changes 2022-04-01 14:33:57 +02:00
holger krekel
c6d901d799 first iteration of faster sorting 2022-04-01 14:33:57 +02:00
B. Petersen
4880f9ff32 improve repl message search
- allow spaces in queries
- show query and scope below result
2022-04-01 11:46:26 +02:00
holger krekel
aaa42a3412 feedback if missing env var 2022-03-31 17:05:33 +02:00
holger krekel
3e5e852e20 1.30 is imminent so i think it makes sense to do a cargo-update now for all deps, to detect issues as early as possible before releases go to stores. 2022-03-31 16:53:38 +02:00
holger krekel
0a3f44bd73 housekeeping cleanup: factor out remove-unused-files logic 2022-03-31 16:45:58 +02:00
holger krekel
d4fed5f5f7 add chatlist loading benchmark 2022-03-31 16:45:45 +02:00
dependabot[bot]
dce7b90fc2 cargo: bump native-tls from 0.2.8 to 0.2.10
Bumps [native-tls](https://github.com/sfackler/rust-native-tls) from 0.2.8 to 0.2.10.
- [Release notes](https://github.com/sfackler/rust-native-tls/releases)
- [Changelog](https://github.com/sfackler/rust-native-tls/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sfackler/rust-native-tls/compare/v0.2.8...v0.2.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-30 20:15:21 +02:00
dependabot[bot]
4f94bdff3f Merge pull request #3160 from deltachat/dependabot/cargo/async-trait-0.1.53 2022-03-29 13:44:16 +00:00
dependabot[bot]
ce1f2a6fd4 Merge pull request #3162 from deltachat/dependabot/cargo/quote-1.0.17 2022-03-29 13:43:22 +00:00
dependabot[bot]
da292bb9b2 cargo: bump quote from 1.0.16 to 1.0.17
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.16 to 1.0.17.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.16...1.0.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-29 08:07:43 +00:00
dependabot[bot]
326a75d0e8 Merge pull request #3161 from deltachat/dependabot/cargo/syn-1.0.90 2022-03-29 08:05:58 +00:00
dependabot[bot]
e47860bc2e cargo: bump syn from 1.0.89 to 1.0.90
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.89 to 1.0.90.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.89...1.0.90)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-28 21:14:00 +00:00
dependabot[bot]
6212151562 cargo: bump async-trait from 0.1.52 to 0.1.53
Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.52 to 0.1.53.
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.52...0.1.53)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-28 21:13:49 +00:00
link2xt
8c2b9f9901 Do not overwrite better_msg if apply_group_changes returns None 2022-03-27 11:23:45 +00:00
link2xt
e9a733a789 Pass better message around instead of mutating mimeparser
This change is aimed at decoupling parsing and
add_parts() stages to eventually separate parsing
from database changes and pipeline message parsing and
decryption.
2022-03-27 11:23:45 +00:00
Floris Bruynooghe
b2fe723570 Do not read whole webxdc file into memory
This seems not only wasteful but genuinly has the risk someone makes
their device useless by accidentally adding a huge file.

This also re-structures the checks a little: The if-conditions are
flattened out and cheap checks are done before more expensive ones.
2022-03-28 14:48:55 +02:00
link2xt
33ba8dabe0 Increase python test timeout 2022-03-27 08:48:43 +00:00
link2xt
0842e54f52 Add ephemeral_timestamp index for msgs table
This reduced get_chat_msgs() benchmark time from 400ms to 170ms.
2022-03-26 20:19:49 +00:00
link2xt
08d34e41c6 Return results from add_parts() via structure
Replaced mutable out parameters with explicit return of structure.
Also moved all decisions about emitted events out of add_parts(). Chat
ID is removed from created_db_entries as it is the same for all parts.
2022-03-26 16:38:08 +00:00
Hocuri
e93c9f74c9 Add get_chat_msgs benchmark (#3151) 2022-03-26 15:18:27 +01:00
bjoern
1ab81256e9 remove usued repl command 'event' (#3153)
no need to re-implement that unless there is actually some need.
2022-03-25 15:53:51 +01:00
dependabot[bot]
cb19de57bb Merge pull request #3144 from deltachat/dependabot/cargo/zip-0.6.0 2022-03-23 10:26:36 +00:00
dependabot[bot]
e678e7df8f Merge pull request #3146 from deltachat/dependabot/cargo/log-0.4.16 2022-03-23 10:22:21 +00:00
bjoern
8487eefe46 config_cache fixes (#3145)
* add simple backup export/import test

this test fails on current master
until the context is recrated.

* avoid config_cache races

adds needed SQL-statements to config_cache locking.

otherwise, another thread may alter the database
eg. between SELECT and the config_cache update -
resulting in the wrong value being written to config_cache.

* also update config_cache on initializing tables

VERSION_CFG is also set later, however,
not doing it here will result in bugs when we change DBVERSION at some point.

as this alters only VERSION_CFG and that is executed sequentially anyway,
race conditions between SQL and config_cache
seems not to be an issue in this case.

* clear config_cache after backup import

import replaces the whole database,
so config_cache needs to be invalidated as well.

we do that before import,
so in case a backup is imported only partly,
the cache does not add additional problems.

* update CHANGELOG
2022-03-22 22:46:29 +01:00
dependabot[bot]
86da1aa429 Merge pull request #3147 from deltachat/dependabot/cargo/async-std-1.11.0 2022-03-22 21:37:45 +00:00
dependabot[bot]
48b580b59e cargo: bump async-std from 1.10.0 to 1.11.0
Bumps [async-std](https://github.com/async-rs/async-std) from 1.10.0 to 1.11.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.10.0...v1.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-22 21:15:44 +00:00
dependabot[bot]
8b568d796e cargo: bump log from 0.4.14 to 0.4.16
Bumps [log](https://github.com/rust-lang/log) from 0.4.14 to 0.4.16.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-22 21:15:36 +00:00
dependabot[bot]
4b5af85094 cargo: bump zip from 0.5.13 to 0.6.0
Bumps [zip](https://github.com/zip-rs/zip) from 0.5.13 to 0.6.0.
- [Release notes](https://github.com/zip-rs/zip/releases)
- [Commits](https://github.com/zip-rs/zip/commits/v0.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-21 21:16:26 +00:00
B. Petersen
8d0be06f45 log file size on backup import
due to an bug from Apple copying files from/to iPhones
(cmp. https://support.delta.chat/t/import-backup-to-ios/1628/7 )
it may easily happen that one gets corrupted/partly backups.

such imports usually fail with some error,
however, for debugging it is nice to have the concrete file size in the log.
2022-03-21 22:09:22 +01:00
link2xt
26ae8accd4 Automatically unblock chats with outgoing messages 2022-03-20 18:03:10 +00:00
Hocuri
321e3e27de Introduce config caching (#3131)
* Introduce config caching

* Changelog

* Update CHANGELOG.md

Co-authored-by: bjoern <r10s@b44t.com>

* Cache a value after reading it

Co-authored-by: bjoern <r10s@b44t.com>
2022-03-21 10:13:43 +00:00
link2xt
7d26968bb3 Try to start ephemeral timers only if some message has nonzero timer 2022-03-20 18:12:01 +00:00
link2xt
83464a882e Optimize markseen_msgs
Use a single SELECT statement for all messages
and start ephemeral timers for all messages at once.
2022-03-20 14:57:14 +00:00
Hocuri
1e94ad25e1 Use repeat_vars() more (#3133) 2022-03-20 15:23:11 +01:00
link2xt
a3ba19db96 Resultify delete_poi_location() 2022-03-19 17:29:54 +00:00
link2xt
d9e9c849e1 imap: do not delete duplicates
Currently if user moves the message into some other folder and then
moves the message back, the message is considered duplicate even
though previous copy was already deleted. This is a common problem
reported by users at least twice.

Keeping duplicates does no harm except for additional storage usage.
If the message is later deleted by the user, all the copies on the
server will be deleted. anyway.
2022-03-19 15:51:17 +00:00
dependabot[bot]
c162c23d9e Merge pull request #3135 from deltachat/dependabot/cargo/tagger-4.3.3 2022-03-18 23:22:23 +00:00
dependabot[bot]
90fd1c300f Merge pull request #3136 from deltachat/dependabot/cargo/quote-1.0.16 2022-03-18 23:21:09 +00:00
dependabot[bot]
902a9cc812 Merge pull request #3137 from deltachat/dependabot/cargo/libc-0.2.121 2022-03-18 23:20:24 +00:00
dependabot[bot]
c51e1805fa cargo: bump libc from 0.2.120 to 0.2.121
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.120 to 0.2.121.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.120...0.2.121)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-18 21:10:46 +00:00
dependabot[bot]
7a2b9e85e7 cargo: bump quote from 1.0.15 to 1.0.16
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.15 to 1.0.16.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.15...1.0.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-18 21:10:40 +00:00
dependabot[bot]
547c40cd52 cargo: bump tagger from 4.3.1 to 4.3.3
Bumps [tagger](https://github.com/tiby312/tagger) from 4.3.1 to 4.3.3.
- [Release notes](https://github.com/tiby312/tagger/releases)
- [Commits](https://github.com/tiby312/tagger/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-18 21:10:30 +00:00
Floris Bruynooghe
e2d631097d Fix master by reverting ContactId Display impl (#3134)
Actual fix needs more investigation, it's not obvious.
2022-03-17 19:29:18 +01:00
Floris Bruynooghe
cc55be0b0a Customise Display impl of ContactId
This brings the Display of ContactId in line with those of ChatId etc,
which is a bit clearer is logs and such places.

It also updates an SQL query to rely on the ToSql impl of ContactId
rather than it's Display when building the query.
2022-03-16 22:41:14 +01:00
dependabot[bot]
64927190bd Merge pull request #3132 from deltachat/dependabot/cargo/syn-1.0.89 2022-03-16 21:39:21 +00:00
dependabot[bot]
24515126fe cargo: bump syn from 1.0.88 to 1.0.89
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.88 to 1.0.89.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.88...1.0.89)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 21:14:45 +00:00
Hocuri
7a56a93028 Fix long filenames containing dots (#3098) 2022-03-16 20:41:24 +01:00
Hocuri
ea7fc3a171 Benchmark dc_receive_imf() (#3128)
Don't count the account creation in the receive emails benchmark

Use Criterion's async support

See https://bheisler.github.io/criterion.rs/book/user_guide/benchmarking_async.html
2022-03-16 20:30:33 +01:00
dependabot[bot]
ae36a26045 cargo: bump image from 0.23.14 to 0.24.1
Bumps [image](https://github.com/image-rs/image) from 0.23.14 to 0.24.1.
- [Release notes](https://github.com/image-rs/image/releases)
- [Changelog](https://github.com/image-rs/image/blob/master/CHANGES.md)
- [Commits](https://github.com/image-rs/image/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 13:10:24 +01:00
dependabot[bot]
d6c9f5c64b cargo: bump textwrap from 0.14.2 to 0.15.0
Bumps [textwrap](https://github.com/mgeisler/textwrap) from 0.14.2 to 0.15.0.
- [Release notes](https://github.com/mgeisler/textwrap/releases)
- [Changelog](https://github.com/mgeisler/textwrap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mgeisler/textwrap/compare/0.14.2...0.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 11:23:12 +01:00
dependabot[bot]
c4f4f4295b cargo: bump async-std-resolver from 0.20.4 to 0.21.1
Bumps [async-std-resolver](https://github.com/bluejekyll/trust-dns) from 0.20.4 to 0.21.1.
- [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.20.4...v0.21.1)

---
updated-dependencies:
- dependency-name: async-std-resolver
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 10:59:56 +01:00
link2xt
a997322efb Update MSRV to 1.56 and current version to 1.59
This is needed to support Rust 2021 edition required by the latest versions of `ed25519` and `image` crates.
2022-03-16 10:56:16 +01:00
dependabot[bot]
799688af76 cargo: bump libc from 0.2.119 to 0.2.120
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.119 to 0.2.120.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.119...0.2.120)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 10:55:58 +01:00
dependabot[bot]
260e95d027 cargo: bump syn from 1.0.86 to 1.0.88
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.86 to 1.0.88.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.86...1.0.88)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 10:55:20 +01:00
Floris Bruynooghe
f9ee70aa2e Minor cleanup of Viewtype
Provide checking for attachment as a method and move it to the message
module.  The method is a lot easier to read and have correct
expectations about.
2022-03-16 10:46:58 +01:00
Hocuri
50f13cb84b Set X-Microsoft-Original-Message-ID on outgoing emails for amazonaws (#3077) 2022-03-13 14:39:49 +01:00
dependabot[bot]
fc7e08bb49 Merge pull request #3087 from deltachat/dependabot/cargo/strum-0.24.0 2022-03-13 11:44:29 +00:00
dependabot[bot]
06ed3e5dfd cargo: bump strum from 0.23.0 to 0.24.0
Bumps [strum](https://github.com/Peternator7/strum) from 0.23.0 to 0.24.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)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-13 10:50:20 +00:00
dependabot[bot]
4d792ad57b Merge pull request #3089 from deltachat/dependabot/cargo/strum_macros-0.24.0 2022-03-13 10:48:56 +00:00
dependabot[bot]
4fa78bfca0 Merge pull request #3114 from deltachat/dependabot/cargo/once_cell-1.10.0 2022-03-13 10:29:47 +00:00
link2xt
2012833cb3 Fix lint 2022-03-12 20:07:00 +00:00
link2xt
e48eef7e32 Start ephemeral timer when seen status is synchronized via imap 2022-03-12 19:28:31 +00:00
bjoern
74ac9c3a92 fix docs: dc_markseen_msgs() is typically called when scrolling through message list, not chat list. (#3120) 2022-03-12 13:45:22 +01:00
Hocuri
a907d789d6 Assign replies from different address to two-member-groups (#3119)
Holger had a case where he wrote with someone using a classing MUA.

He opened a two-member-group with this person (which also allowed him to
set the subject).

At some point the other person replied from a different email address.

What he expected: This reply should be sorted into the two-member-group.
What happened: This reply was sorted into the 1:1 chat.

---

I had added the line && chat_contacts.contains(&from_id) months ago when I wrote
this code because it seemed vaguely sensible but without any real
reason. So, let's remove it and see if it creates other problems -
my gut feeling is no.
2022-03-12 10:47:58 +00:00
dependabot[bot]
fc46c0b49c Merge pull request #3121 from deltachat/dependabot/cargo/tagger-4.3.1 2022-03-12 10:36:51 +00:00
dependabot[bot]
fef7862045 cargo: bump tagger from 4.2.1 to 4.3.1
Bumps [tagger](https://github.com/tiby312/tagger) from 4.2.1 to 4.3.1.
- [Release notes](https://github.com/tiby312/tagger/releases)
- [Commits](https://github.com/tiby312/tagger/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-11 21:23:15 +00:00
Hocuri
d9441a6bdd Also resync UIDs in folders that are not configured (#2289) 2022-03-10 16:12:24 +01:00
Simon Laux
332cb0896b add note about perl requirement to readme
closes #3106
2022-03-10 12:53:34 +01:00
dependabot[bot]
d1b0c28924 Merge pull request #3084 from deltachat/dependabot/cargo/libc-0.2.119 2022-03-09 11:28:43 +00:00
dependabot[bot]
dce958aac4 Merge pull request #3115 from deltachat/dependabot/cargo/regex-1.5.5 2022-03-09 11:26:52 +00:00
Floris Bruynooghe
438940219e Introduce a ContactId newtype
This makes the contact ID its own newtype instead of being a plain
u32.  The change purposefully does not yet try and reap any benefits
from this yet, instead aiming for a boring change that's easy to
review.  Only exception is the ToSql/FromSql as not doing that yet
would also have created churn in the database code and it is easier to
go straight for the right solution here.
2022-03-08 22:57:51 +01:00
link2xt
f28fcec81d imap: do not treat messages without Message-ID as duplicates
Message-IDs are now retrieved only during fetching and saved into imap
table. dc_receive_imf_inner does not attempt to extract the Message-ID
anymore.

For messages without Message-ID the ID is now generated in
imap::fetch_new_messages rather than dc_receive_imf_inner,
so the same ID is used in the imap table (maintained by the imap
module) and msgs table (maintained by dc_receive_imf module).

Message-ID generation based on the Date, From and To field hashing has
been replaced with a simple dc_create_id() to avoid retrieving Date,
From, and To fields in the imap module, as it's hard to test that it
stays compatible between Delta Chat versions in this module. This
breaks jump-to-quote for quoted messages without Message-ID, which is
not critical.

Also prefetch X-Microsoft-Original-Message-ID, so retrieval of
duplicate messages with X-Microsoft-Original-Message-ID can be skipped
like it is done for messages with Message-ID header.
2022-03-08 15:23:22 +00:00
missytake
586d027f86 Merge pull request #3103 from deltachat/docs-gh-action
GitHub Action to build & upload the rust documentation to rs.delta.chat
2022-03-08 16:02:57 +01:00
gerryfrancis
bd4fb7486d Various corrections #1 (#2983)
* Various corrections

Monk business... ;)

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* Update deltachat.h

* use correct spelling for parameter name

Co-authored-by: B. Petersen <r10s@b44t.com>
2022-03-08 14:23:40 +00:00
dependabot[bot]
f9cd2b8f36 cargo: bump regex from 1.5.4 to 1.5.5
Bumps [regex](https://github.com/rust-lang/regex) from 1.5.4 to 1.5.5.
- [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.5.4...1.5.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-08 14:08:48 +00:00
dependabot[bot]
62e22236b7 Merge pull request #3076 from deltachat/dependabot/cargo/sha2-0.10.2 2022-03-08 14:07:31 +00:00
dependabot[bot]
8b157f427a cargo: bump once_cell from 1.9.0 to 1.10.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.9.0 to 1.10.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.9.0...v1.10.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>
2022-03-08 14:05:07 +00:00
dependabot[bot]
f165c1d9b0 Merge pull request #3110 from deltachat/dependabot/cargo/anyhow-1.0.56 2022-03-08 14:03:59 +00:00
bjoern
500e2d62a0 remove sentbox_move (#3111)
* remove SentboxMove

* adapt python test to removed sendbox_move option

* update CHANGELOG
2022-03-08 11:29:45 +01:00
bjoern
a06e8677ac Fix link to Mozilla (#3112)
it seems to be a bug on the Mozilla servers,
however, they take months to fix that, cmp.
https://bugzilla.mozilla.org/show_bug.cgi?id=1744432
so we just use archive.org for now.
2022-03-08 01:12:19 +01:00
dependabot[bot]
b4d5783928 cargo: bump anyhow from 1.0.53 to 1.0.56
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.53 to 1.0.56.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.53...1.0.56)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-07 21:24:16 +00:00
missytake
3ce7f45503 use rust toolchain of deltachat-core-rust 2022-03-06 13:28:06 +01:00
missytake
b436c2761a GitHub Action to build & upload the CFFI documentation 2022-03-06 09:59:32 +01:00
missytake
b586d3bb0e only build docs for deltachat crate 2022-03-06 09:43:39 +01:00
holger krekel
63688a2f95 remove getAllUpdates() and add a typical replicatio API for the update call (#3081)
* (r10s, adb, hpk) remove getAllUpdates() and add a typical replica-API that works with increasing serials.  Streamline docs a bit.

* adapt ffi to new api

* documentation: updates serials may have gaps

* get_webxdc_status_updates() return updates larger than a given serial

* remove status_update_id from status-update-event; it is not needed (ui should update from the last known serial) and easily gets confused with last_serial

* unify wording to 'StatusUpdateSerial'

* remove legacy payload format, all known webxdc should be adapted

* add serial and max_serial to status updates

* avoid races when getting max_serial by avoiding two SQL calls

* update changelog

Co-authored-by: B. Petersen <r10s@b44t.com>
2022-03-04 20:22:48 +01:00
missytake
379cb1b2e0 remove trailing slash, so it doesn't just copy the content of doc/ 2022-03-04 01:58:33 +01:00
missytake
78429492f1 fix: pass arguments to rsync github action 2022-03-04 01:45:05 +01:00
missytake
9875047674 docs github action: scp -> rsync 2022-03-04 01:31:00 +01:00
missytake
5014b0a9cb GitHub Action to build & upload the rust documentation 2022-03-03 18:11:55 +01:00
Floris Bruynooghe
ef841b1aa3 Securejoin: store bobstate in database instead of context
The state bob needs to maintain during a secure-join process when
exchanging messages used to be stored on the context.  This means if
the process was killed this state was lost and the securejoin process
would fail.  Moving this state into the database should help this.

This still only allows a single securejoin process at a time, this may
be relaxed in the future.  For now any previous securejoin process
that was running is killed if a new one is started (this was already
the case).

This can remove some of the complexity around BobState handling: since
the state is in the database we can already make state interactions
transactional and correct.  We no longer need the mutex around the
state handling.  This means the BobStateHandle construct that was
handling the interactions between always having a valid state and
handling the mutex is no longer needed, resulting in some nice
simplifications.

Part of #2777.
2022-03-01 23:02:40 +01:00
link2xt
368f27ffbc Update rusqlite to stable version 2022-02-27 20:00:35 +00:00
link2xt
0e50bc1443 Fix 1.59 clippy warnings 2022-02-27 13:29:02 +00:00
Hocuri
7c4a6ddcdf Add AcManager (#3073)
* Add AcManager

See https://github.com/deltachat/deltachat-core-rust/pull/2901#issuecomment-998285039

This reduces boilerplate code again therefore, improving the
signal-noise-ratio and reducing the mental barrier to start
writing a unit test.

Slightly off-topic:

I didn't add any advanced functions like `manager.get("alice");` because
they're not needed yet; however, once we have the AcManager we can
think about fancy things like:

```rust
acm.send_text(&alice, "Hi Bob, this is Alice!", &bob);
```
which automatically lets bob receive the message.

However, this may be less useful than it seems at first, since most of
the tests I looked at wouldn't benefit from it, so at least I won't do
it until I have a test that would benefit from it.

* Remove unnecessary RefCell

* Rename AcManager to TestContextManager

* Don't store TestContext's in a vec for now as we don't need this; we can re-add it later

* Rename acm -> tcm
2022-02-23 19:34:47 +01:00
link2xt
7ab6d95b6c mimefactory: place common IMF headers at the top of the message
This moves most common headers like From, To, Subject etc. defined in
the Internet Message Format standard at the top of the message
in the same order as used in RFC 5322.
2022-02-23 17:51:15 +00:00
link2xt
6c32b89906 smtp: add more logging 2022-02-23 17:04:30 +00:00
dependabot[bot]
056e659a20 cargo: bump strum_macros from 0.23.1 to 0.24.0
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.23.1 to 0.24.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)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-22 21:14:05 +00:00
dependabot[bot]
62baff665c cargo: bump libc from 0.2.117 to 0.2.119
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.117 to 0.2.119.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.117...0.2.119)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-21 21:14:06 +00:00
bjoern
7c5eb0ae37 prepare 1.76 (#3082)
* update changelog for 1.76.0

* bump version to 1.76.0
2022-02-20 22:58:46 -05:00
link2xt
36bce6c468 Remove unused async-std feature 2022-02-19 11:30:48 +00:00
dependabot[bot]
65df02163d cargo: bump sha2 from 0.10.1 to 0.10.2
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.10.1 to 0.10.2.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.1...sha2-v0.10.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-17 21:14:06 +00:00
Hocuri
19a32cdfd3 Let MS Exchange MDNs mark the In-Reply-To message as read (#3075)
Fix https://github.com/deltachat/deltachat-core-rust/issues/2891
2022-02-17 09:56:00 +01:00
link2xt
d708f386a1 smtp: set message state to failed when retry limit is exceeded 2022-02-13 08:59:52 +00:00
link2xt
0f837f4bed Fix a comment typo 2022-02-12 16:29:42 +00:00
link2xt
242e8e2bb3 smtp: remove the message in case of permanent failure
When `smtp_send` returns `Status::Finished`,
the message should be removed from the queue even in case of
failure, such as a permanent error.

In addition to this bugfix, move the retry count increase to
the beginning of `send_msg_to_smtp` to ensure no message is
retried infinitely even in case of similar bugs.
2022-02-12 16:20:13 +00:00
link2xt
1d56b24b67 cargo update 2022-02-12 16:19:44 +00:00
Hocuri
bb9138708a Fix disappearing drafts (#3067) 2022-02-10 10:05:30 +01:00
Hocuri
34f5510f1f Don't directly download messages from the Spam folder (#3015)
fix #3007

My approach is:

We don't download any messages from the spam folder anymore, and only download them if they were moved out. This means that is-it-spam logic only resides in spam_target_folder(). This has some implications, see the comments.
2022-02-10 09:06:22 +01:00
link2xt
6c6d47c89c Fix CI
timeout_func_only makes pytest-rerunfailures work with pytest-timeout,
but it only works with default timeout_method.

See pytest-rerunfailures issue for details:
https://github.com/pytest-dev/pytest-rerunfailures/issues/99
2022-02-08 20:50:11 +00:00
link2xt
196075c031 imap: batch message deletion 2022-02-06 11:42:30 +00:00
link2xt
2e5e8f73c6 imap: simplify get_quota_roots() 2022-02-06 15:17:05 +00:00
link2xt
ada5d38272 imap: remove unwrap() 2022-02-06 14:07:04 +00:00
link2xt
c4b0f773db python: remove arbitrary timeouts from tests
pytest-timeout already handles all deadlocks and is configurable with
--timeout option. With this change it is possible to disable timeout
with --timeout 0 to run tests on extremely slow connections.
2022-02-06 12:52:48 +00:00
link2xt
276daf631e imap: move messages in batches
Also change how NO response is treated. NO response means there is an
error moving/copying the messages. When there are no matching
messages, the response is "OK No matching messages, so nothing copied"
according to some RFC 9051 examples.
2022-02-05 22:15:46 +00:00
link2xt
fb19b58147 Reduce number of unsafe as conversions
Enable clippy::cast_lossless lint and get rid of
some conversions pointed out by  clippy::as_conversions.
2022-02-05 12:42:14 +00:00
dependabot[bot]
13a5e3cf6f Merge pull request #3055 from deltachat/dependabot/cargo/async-std-resolver-0.20.4 2022-02-04 21:39:59 +00:00
bjoern
1caf3caf1b do set_visibility() in a transaction (#3053)
this avoids archived chats containing fresh messages:

before, it could happen that between the two SQL calls
a new fresh message arrives,
unarchives the chat that is immediately archived by the second SQL call -
resulting in an archive chat containing fresh messages.

as fresh messages counter are shown on app icon etc.
this is pretty weird for the user as they do not see what is "fresh".

the other way round,
there is no transaction in receive_imf(),
however, receive_imf() only unarchives chats,
so that is visible and no big issue for the user.

the issue is rare at all,
however, annoying if you get that as the badge counter may be stuck at "1"
nearly forever (until you open the archived chat in question).
2022-02-03 20:40:24 +01:00
dependabot[bot]
564370f79a cargo: bump async-std-resolver from 0.20.3 to 0.20.4
Bumps [async-std-resolver](https://github.com/bluejekyll/trust-dns) from 0.20.3 to 0.20.4.
- [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.20.3...v0.20.4)

---
updated-dependencies:
- dependency-name: async-std-resolver
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-02 21:14:26 +00:00
bjoern
24e749a2c9 prepare 1.75 (#3049)
* update changelog for 1.75.0

* bump version to 1.75.0
2022-02-01 14:00:40 +01:00
link2xt
cccdc51ad4 Optimize delete_expired_imap_messages()
For me this reduced query time from 0.3 s to 0.05 s.
2022-01-31 20:34:01 +00:00
bjoern
99ddce6c3e prepare 1.74 (#3046)
* update changelog for 1.74.0

* bump version to 1.74.0
2022-01-31 19:53:50 +01:00
link2xt
f68088cfb5 imap: avoid reconnection loop when message without Message-ID is marked as seen
- do not attempt to mark reserved meessages as seen when
  messages with empty Message-ID are marked as seen on IMAP
- do not reconnect on Seen flag synchronization failures This avoid
  reconnection loops in case of permanent errors in `sync_seen_flags`
2022-01-31 00:00:00 +00:00
Hocuri
c8f56d748a Only fetch mvbox deltachat.h additions (#3045)
* Use the formatting of the rest of the file

* Add changes require restarting IO by calling dc_stop_io() and then dc_start_io(). comment
2022-01-31 17:46:18 +01:00
bjoern
a43fc47bb6 update provider database (#3043)
* update provider database

ran `./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs`

* update changelog
2022-01-31 16:07:20 +01:00
bjoern
8c1bfac53b prepare 1.73 (#3042)
* update changelog for 1.73.0

* bump version to 1.73.0
2022-01-31 15:12:44 +01:00
Floris Bruynooghe
97853c3660 Flub/watch mvbox only (#3028)
* Make set_config() look a bit nicer

* Add OnlyFetchMvbox option

* Add test for the config

* Add option to only watch mvbox

This is supposed to support having a server-side rule which moves
emails to the mvbox already.  The new option makes sure the mvbox is
wathched and also makes sure no messages are feched from folders other
than the mvbox and the spam folder if enabled.  It does not interact
with the other settings.

* Fixup ignore conditions

* Cleanup some bits

* Watch the mvbox when `WatchMvboxOnly` is set

* Rename back to only_fetch_mvbox (flub said it's OK for him)

* typo

* clippy, more typos

Co-authored-by: Hocuri <hocuri@gmx.de>
2022-01-31 13:39:48 +01:00
link2xt
f304a30193 imap: fetch Inbox before scanning other folders 2022-01-31 12:03:21 +01:00
link2xt
7eadca3959 imap: do not synchronize Seen flags on unwatched folders
Synchronizing seen flags doubles the time required to scan all
folders. Delta Chat only marks messages as Seen on Inbox or DeltaChat,
so there is no need to check for Seen flag on other folders.
2022-01-30 20:00:00 +00:00
link2xt
5b131cf77c Do not generate QR codes for ad-hoc groups 2021-11-07 15:55:47 +00:00
link2xt
ce6ec64069 Do not assign group IDs to ad hoc groups 2021-11-07 15:55:47 +00:00
159 changed files with 20745 additions and 9120 deletions

View File

@@ -3,7 +3,7 @@ updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
interval: "monthly"
commit-message:
prefix: "cargo"
open-pull-requests-limit: 10
open-pull-requests-limit: 50

View File

@@ -45,7 +45,7 @@ jobs:
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --tests --examples
args: --workspace --tests --examples --benches
docs:
name: Rust doc comments
@@ -77,19 +77,19 @@ jobs:
include:
# Currently used Rust version, same as in `rust-toolchain` file.
- os: ubuntu-latest
rust: 1.54.0
rust: 1.61.0
python: 3.9
- os: windows-latest
rust: 1.54.0
rust: 1.61.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.51.0
# Minimum Supported Rust Version = 1.56.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.51.0
rust: 1.56.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:

21
.github/workflows/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Dependabot auto-approve
on: pull_request
permissions:
pull-requests: write
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1.1.1
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve a PR
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

View File

@@ -0,0 +1,32 @@
# documentation: https://github.com/deltachat/sysadmin/tree/master/download.delta.chat
name: Delete node PR previews
on:
pull_request:
types: [closed]
jobs:
delete:
runs-on: ubuntu-latest
steps:
- name: Get Pullrequest ID
id: getid
run: |
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
echo ::set-output name=prid::$PULLREQUEST_ID
- name: Renaming
run: |
# create empty file to copy it over the outdated deliverable on download.delta.chat
echo "This preview build is outdated and has been removed." > empty
cp empty deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz
- name: Replace builds with dummy files
uses: horochx/deploy-via-scp@v1.0.1
with:
user: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
host: "download.delta.chat"
port: 22
local: "deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz"
remote: "/var/www/html/download/node/"

34
.github/workflows/node-docs.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Generate & upload node.js documentation
on:
push:
branches:
- master
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: npm install and generate documentation
run: |
cd node
npm i --ignore-scripts
npx typedoc
mv docs js
- name: Upload
uses: horochx/deploy-via-scp@v1.0.1
with:
user: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
host: "delta.chat"
port: 22
local: "node/js"
remote: "/var/www/html/"

166
.github/workflows/node-package.yml vendored Normal file
View File

@@ -0,0 +1,166 @@
name: 'node.js'
on:
pull_request:
push:
tags:
- '*'
- '!py-*'
jobs:
prebuild:
name: 'prebuild'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-18.04, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: System info
run: |
rustc -vV
rustup -vV
cargo -vV
npm --version
node --version
- name: Cache node modules
uses: actions/cache@v2
with:
path: |
${{ env.APPDATA }}/npm-cache
~/.npm
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v2
with:
path: |
~/.cargo/registry/
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2
- name: Install dependencies & build
if: steps.cache.outputs.cache-hit != 'true'
run: |
cd node
npm install --verbose
- name: Build Prebuild
run: |
cd node
npm run prebuildify
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
- name: Upload Prebuild
uses: actions/upload-artifact@v1
with:
name: ${{ matrix.os }}
path: node/${{ matrix.os }}.tar.gz
pack-module:
needs: prebuild
name: 'Package deltachat-node and upload to download.delta.chat'
runs-on: ubuntu-18.04
steps:
- name: install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: get tag
id: tag
uses: dawidd6/action-get-tag@v1
continue-on-error: true
- name: Get Pullrequest ID
id: prepare
run: |
tag=${{ steps.tag.outputs.tag }}
if [ -z "$tag" ]; then
node -e "console.log('DELTACHAT_NODE_TAR_GZ=deltachat-node-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
else
echo "No preview will be uploaded this time, but the $tag release"
fi
- name: System info
run: |
rustc -vV
rustup -vV
cargo -vV
npm --version
node --version
echo $DELTACHAT_NODE_TAR_GZ
- name: Download ubuntu prebuild
uses: actions/download-artifact@v1
with:
name: ubuntu-18.04
- name: Download macos prebuild
uses: actions/download-artifact@v1
with:
name: macos-latest
- name: Download windows prebuild
uses: actions/download-artifact@v1
with:
name: windows-latest
- shell: bash
run: |
mkdir node/prebuilds
tar -xvzf ubuntu-18.04/ubuntu-18.04.tar.gz -C node/prebuilds
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
tree node/prebuilds
rm -rf ubuntu-18.04 macos-latest windows-latest
- name: install dependencies without running scripts
run: |
npm install --ignore-scripts
- name: build typescript part
run: |
npm run build:bindings:ts
- name: package
shell: bash
run: |
mv node/README.md README.md
npm pack .
ls -lah
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v1
with:
name: deltachat-node.tgz
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
# Upload to download.delta.chat/node/preview/
- name: Upload deltachat-node preview to download.delta.chat/node/preview/
id: upload-preview
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
if [[ -z "$DELTACHAT_NODE_TAR_GZ" ]]
then
exit 1
fi
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
continue-on-error: true
- name: "Post links to details"
if: steps.upload-preview.outcome == 'success'
run: node ./node/scripts/postLinksToDetails.js
env:
URL: preview/${{ env.DELTACHAT_NODE_TAR_GZ }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Upload to download.delta.chat/node/
- name: Upload deltachat-node build to download.delta.chat/node/
if: ${{ steps.tag.outputs.tag }}
id: upload
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
$DELTACHAT_NODE_TAG_TAR_GZ=deltachat-node-${{ steps.tag.outputs.tag }}.tar.gz
mv $DELTACHAT_NODE_TAR_GZ $DELTACHAT_NODE_TAG_TAR_GZ
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAG_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"

67
.github/workflows/node-tests.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: 'node.js'
on:
pull_request:
push:
branches:
- master
- staging
- trying
jobs:
prebuild:
name: 'tests'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-18.04, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: System info
run: |
rustc -vV
rustup -vV
cargo -vV
npm --version
node --version
- name: Cache node modules
uses: actions/cache@v2
with:
path: |
${{ env.APPDATA }}/npm-cache
~/.npm
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v2
with:
path: |
~/.cargo/registry/
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2
- name: Install dependencies & build
if: steps.cache.outputs.cache-hit != 'true'
run: |
cd node
npm install --verbose
- name: Test
if: runner.os != 'Windows'
run: |
cd node
npm run test
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
- name: Run tests on Windows, except lint
if: runner.os == 'Windows'
run: |
cd node
npm run test:mocha
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}

28
.github/workflows/upload-docs.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Build & Deploy Documentation on rs.delta.chat
on:
push:
branches:
- master
- docs-gh-action
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat --no-deps
- name: Upload to rs.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/rs/"

28
.github/workflows/upload-ffi-docs.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Build & Deploy Documentation on cffi.delta.chat
on:
push:
branches:
- master
- docs-gh-action
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/cffi/"

11
.gitignore vendored
View File

@@ -29,3 +29,14 @@ deltachat-ffi/xml
coverage/
.DS_Store
.vscode/launch.json
python/accounts.txt
python/all-testaccounts.txt
tmp/
# from deltachat-node
node_modules/
node/build/
node/dist/
node/prebuilds/
node/.nyc_output/

42
.npmignore Normal file
View File

@@ -0,0 +1,42 @@
.circleci/
.gitmodules
node/.nyc_output/
.travis.yml
appveyor.yml
node/build/
node/README.md
rustfmt.toml
spec.md
test-data/
build2/
node_modules
.git
.idea/
.pytest_cache
CMakeLists.txt
README.md
contrib/
node/ci_scripts/
coverage/
node/.circleci
node/appveyor.yml
ci/
ci_scripts/
python/
target
proptest-regressions
deltachat-ffi/Doxyfile
scripts
webxdc.md
standards.md
draft/
node/examples/
# deltachat-core-rust/assets # don't exclude assets, otherwise it won't build
node/images/
node/test/
node/windows.md
node/*.tar.gz
node/old_docs.md
.vscode/
.github/
node/.prettierrc.yml

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
package-lock=false

View File

@@ -2,19 +2,218 @@
## Unreleased
## 1.84.0
### Changes
- refactorings #3354 #3347 #3353 #3346
### Fixes
- do not unnecessarily SELECT folders if there are no operations planned on
them #3333
- trim chat encryption info #3350
- fix failure to decrypt first message to self after key synchronization
via Autocrypt Setup Message #3352
- Keep pgp key when you change your own email address #3351
- Do not ignore Sent and Spam folders on Gmail #3369
- handle decryption errors explicitly and don't get confused by encrypted mail attachments #3374
## 1.83.0
### Fixes
- fix node prebuild & package ci #3337
## 1.82.0
### API-Changes
- re-add removed DC_MSG_ID_MARKER1 as in use on iOS #3330
### Changes
- refactorings #3328
### Fixes
- fix node package ci #3331
- fix race condition in ongoing process (import/export, configuration) allocation #3322
## 1.81.0
### API-Changes
- deprecate unused `marker1before` argument of `dc_get_chat_msgs`
and remove `DC_MSG_ID_MARKER1` constant #3274
### Changes
- now the node-bindings are also part of this repository 🎉 #3283
- support `source_code_url` from Webxdc manifests #3314
- support Webxdc document names and add `document` to `dc_msg_get_webxdc_info()` #3317 #3324
- improve chat encryption info, make it easier to find contacts without keys #3318
- improve error reporting when creating a folder fails #3325
- node: remove unmaintained coverage scripts
- send normal messages with higher priority than MDNs #3243
- make Scheduler stateless #3302
- abort instead of unwinding on panic #3259
- improve python bindings #3297 #3298
- improve documentation #3307 #3306 #3309 #3319 #3321
- refactorings #3304 #3303 #3323
### Fixes
- node: throw error when getting context with an invalid account id
- node: throw error when instanciating a wrapper class on `null` (Context, Message, Chat, ChatList and so on)
- use same contact-color if email address differ only in upper-/lowercase #3327
- repair encrypted mails "mixed up" by Google Workspace "Append footer" function #3315
## 1.80.0
### Changes
- update provider database #3284
- improve python bindings, tests and ci #3287 #3286 #3287 #3289 #3290 #3292
### Fixes
- fix escaping in generated QR-code-SVG #3295
## 1.79.0
### Changes
- Send locations in the background regardless of SMTP loop activity #3247
- refactorings #3268
- improve tests and ci #3266 #3271
### Fixes
- simplify `dc_stop_io()` and remove potential panics and race conditions #3273
- fix correct message escaping consisting of a dot in SMTP protocol #3265
## 1.78.0
### API-Changes
- replaced stock string `DC_STR_ONE_MOMENT` by `DC_STR_NOT_CONNECTED` #3222
- add `dc_resend_msgs()` #3238
- `dc_provider_new_from_email()` does no longer do an DNS lookup for checking custom domains,
this is done by `dc_provider_new_from_email_with_dns()` now #3256
### Changes
- introduce multiple self addresses with the "configured" address always being the primary one #2896
- Further improve finding the correct server after logging in #3208
- `get_connectivity_html()` returns HTML as non-scalable #3213
- add update-serial to `DC_EVENT_WEBXDC_STATUS_UPDATE` #3215
- Speed up message receiving via IMAP a bit #3225
- mark messages as seen on IMAP in batches #3223
- remove Received: based draft detection heuristic #3230
- Use pkgconfig for building Python package #2590
- don't start io on unconfigured context #2664
- do not assign group IDs to ad-hoc groups #2798
- dynamic libraries use dylib extension on Darwin #3226
- refactorings #3217 #3219 #3224 #3235 #3239 #3244 #3254
- improve documentation #3214 #3220 #3237
- improve tests and ci #3212 #3233 #3241 #3242 #3252 #3250 #3255 #3260
### Fixes
- Take `delete_device_after` into account when calculating ephemeral loop timeout #3211 #3221
- Fix a bug where a blocked contact could send a contact request #3218
- Make sure, videochat-room-names are always URL-safe #3231
- Try removing account folder multiple times in case of failure #3229
- Ignore messages from all spam folders if there are many #3246
- Hide location-only messages instead of displaying empty bubbles #3248
## 1.77.0
### API changes
- change semantics of `dc_get_webxdc_status_updates()` second parameter
and remove update-id from `DC_EVENT_WEBXDC_STATUS_UPDATE` #3081
### Changes
- add more SMTP logging #3093
- place common headers like `From:` before the large `Autocrypt:` header #3079
- keep track of securejoin joiner status in database to survive restarts #2920
- remove never used `SentboxMove` option #3111
- improve speed by caching config values #3131 #3145
- optimize `markseen_msgs` #3141
- automatically accept chats with outgoing messages #3143
- `dc_receive_imf` refactorings #3154 #3156 #3159
- add index to speedup deletion of expired ephemeral messages #3155
- muted chats stay archived on new messages #3184
- support `min_api` from Webxdc manifests #3206
- do not read whole webxdc file into memory #3109
- improve tests, refactorings #3073 #3096 #3102 #3108 #3139 #3128 #3133 #3142 #3153 #3151 #3174 #3170 #3148 #3179 #3185
- improve documentation #2983 #3112 #3103 #3118 #3120
### Fixes
- speed up loading of chat messages by a factor of 20 #3171 #3194 #3173
- fix an issue where the app crashes when trying to export a backup #3195
- hopefully fix a bug where outgoing messages appear twice with Amazon SES #3077
- do not delete messages without Message-IDs as duplicates #3095
- assign replies from a different email address to the correct chat #3119
- assing outgoing private replies to the correct chat #3177
- start ephemeral timer when seen status is synchronized via IMAP #3122
- do not create empty contact requests with "setup changed" messages;
instead, send a "setup changed" message into all chats we share with the peer #3187
- do not delete duplicate messages on IMAP immediately to accidentally deleting
the last copy #3138
- clear more columns when message expires due to `delete_device_after` setting #3181
- do not try to use stale SMTP connections #3180
- slightly improve finding the correct server after logging in #3207
- retry message sending automatically if loop is not interrupted #3183
- fix a bug where sometimes the file extension of a long filename containing a dot was cropped #3098
## 1.76.0
### Changes
- move messages in batches #3058
- delete messages in batches #3060
- python: remove arbitrary timeouts from tests #3059
- refactorings #3026
### Fixes
- avoid archived, fresh chats #3053
- Also resync UIDs in folders that are not configured #2289
- treat "NO" IMAP response to MOVE and COPY commands as an error #3058
- Fix a bug where messages in the Spam folder created contact requests #3015
- Fix a bug where drafts disappeared after some days #3067
- Parse MS Exchange read receipts and mark the original message as read #3075
- do not retry message sending infinitely in case of permanent SMTP failure #3070
- set message state to failed when retry limit is exceeded #3072
## 1.75.0
### Changes
- optimize `delete_expired_imap_messages()` #3047
## 1.74.0
### Fixes
- avoid reconnection loop when message without Message-ID is marked as seen #3044
## 1.73.0
### API changes
- added `only_fetch_mvbox` config #3028
### Changes
- don't watch Sent folder by default #3025
- use webxdc app name in chatlist/quotes/replies etc. #3027
- refactorings #3023
- remove direct dependency on `byteorder` crate #3031
- make it possible to cancel message sending by removing the message #3034,
this was previosuly removed in 1.71.0 #2939
- synchronize Seen flags only on watched folders to speed up
folder scanning #3041
- remove direct dependency on `byteorder` crate #3031
- refactorings #3023 #3013
- update provider database #3043
- improve documentation #3017 #3018 #3021
### Fixes
- fix splitting off text from webxdc messages #3032
- call slow `delete_expired_imap_messages()` less often #3037
- make synchronization of Seen status more robust in case unsolicited FETCH
result without UID is returned #3022
- fetch Inbox before scanning folders to ensure iOS does
not kill the app before it gets to fetch the Inbox in background #3040
## 1.72.0

View File

@@ -4,10 +4,18 @@ include(GNUInstallDirs)
find_program(CARGO cargo)
if(APPLE)
set(DYNAMIC_EXT "dylib")
elseif(UNIX)
set(DYNAMIC_EXT "so")
else()
set(DYNAMIC_EXT "dll")
endif()
add_custom_command(
OUTPUT
"target/release/libdeltachat.a"
"target/release/libdeltachat.so"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/pkgconfig/deltachat.pc"
COMMAND
PREFIX=${CMAKE_INSTALL_PREFIX}
@@ -32,11 +40,11 @@ add_custom_target(
ALL
DEPENDS
"target/release/libdeltachat.a"
"target/release/libdeltachat.so"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/pkgconfig/deltachat.pc"
)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.so" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

689
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,18 @@
[package]
name = "deltachat"
version = "1.72.0"
version = "1.84.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
edition = "2021"
license = "MPL-2.0"
resolver = "2"
rust-version = "1.56"
[profile.dev]
debug = 0
panic = 'abort'
[profile.release]
lto = true
[patch.crates-io]
rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch="master" }
panic = 'abort'
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
@@ -22,9 +21,9 @@ ansi_term = { version = "0.12.1", optional = true }
anyhow = "1"
async-imap = { git = "https://github.com/async-email/async-imap" }
async-native-tls = { version = "0.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
async-std-resolver = "0.20"
async-std = { version = "1", features = ["unstable"] }
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", default-features=false, features = ["smtp-transport", "socks5"] }
async-std-resolver = "0.21"
async-std = { version = "1" }
async-tar = { version = "0.4", default-features=false }
async-trait = "0.1"
backtrace = "0.3"
@@ -37,26 +36,26 @@ encoded-words = { git = "https://github.com/async-email/encoded-words", branch="
escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
log = {version = "0.4.8", optional = true }
log = {version = "0.4.16", optional = true }
mailparse = "0.13"
native-tls = "0.2"
num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.9.0"
once_cell = "1.10.0"
percent-encoding = "2.0"
pgp = { version = "0.7", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.22"
r2d2 = "0.8"
r2d2_sqlite = "0.19"
r2d2_sqlite = "0.20"
rand = "0.7"
regex = "1.5"
rusqlite = { version = "0.26", features = ["sqlcipher"] }
rusqlite = { version = "0.27", features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustyline = { version = "9", optional = true }
sanitize-filename = "0.3"
@@ -65,9 +64,8 @@ serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
smallvec = "1"
stop-token = "0.7"
strum = "0.23"
strum_macros = "0.23"
strum = "0.24"
strum_macros = "0.24"
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
thiserror = "1"
toml = "0.5"
@@ -76,14 +74,14 @@ uuid = { version = "0.8", features = ["serde", "v4"] }
fast-socks5 = "0.4"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "4.2.1"
textwrap = "0.14.2"
zip = { version = "0.5.13", default-features = false, features = ["deflate"] }
tagger = "4.3.3"
textwrap = "0.15.0"
zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
[dev-dependencies]
ansi_term = "0.12.0"
async-std = { version = "1", features = ["unstable", "attributes"] }
criterion = "0.3"
criterion = { version = "0.3.4", features = ["async_std"] }
futures-lite = "1.12"
log = "0.4"
pretty_env_logger = "0.4"
@@ -119,6 +117,18 @@ harness = false
name = "search_msgs"
harness = false
[[bench]]
name = "receive_emails"
harness = false
[[bench]]
name = "get_chat_msgs"
harness = false
[[bench]]
name = "get_chatlist"
harness = false
[features]
default = ["vendored"]
internals = []

View File

@@ -12,6 +12,8 @@ To download and install the official compiler for the Rust programming language,
$ curl https://sh.rustup.rs -sSf | sh
```
> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
## Using the CLI client
Compile and run Delta Chat Core command line utility, using `cargo`:
@@ -126,7 +128,7 @@ $ cargo test -- --ignored
Language bindings are available for:
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- **Node.js** \[[📂 source](https://github.com/deltachat/deltachat-node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- **Node.js** \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go** \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Free Pascal** \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]

40
benches/get_chat_msgs.rs Normal file
View File

@@ -0,0 +1,40 @@
use async_std::path::Path;
use criterion::async_executor::AsyncStdExecutor;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
let id = 100;
let context = Context::new(dbfile.into(), id).await.unwrap();
for c in chats.iter().take(10) {
black_box(chat::get_chat_msgs(&context, *c, 0).await.ok());
}
}
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") {
let chats: Vec<_> = async_std::task::block_on(async {
let context = Context::new((&path).into(), 100).await.unwrap();
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
let len = chatlist.len();
(0..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
});
c.bench_function("chat::get_chat_msgs (load messages from 10 chats)", |b| {
b.to_async(AsyncStdExecutor)
.iter(|| get_chat_msgs_benchmark(black_box(path.as_ref()), black_box(&chats)))
});
} else {
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
}
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

27
benches/get_chatlist.rs Normal file
View File

@@ -0,0 +1,27 @@
use criterion::async_executor::AsyncStdExecutor;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
async fn get_chat_list_benchmark(context: &Context) {
Chatlist::try_load(context, 0, None, None).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") {
let context =
async_std::task::block_on(async { Context::new(path.into(), 100).await.unwrap() });
c.bench_function("chatlist:try_load (Get Chatlist)", |b| {
b.to_async(AsyncStdExecutor)
.iter(|| get_chat_list_benchmark(black_box(&context)))
});
} else {
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
}
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

84
benches/receive_emails.rs Normal file
View File

@@ -0,0 +1,84 @@
use async_std::{path::PathBuf, task::block_on};
use criterion::{
async_executor::AsyncStdExecutor, black_box, criterion_group, criterion_main, BatchSize,
Criterion,
};
use deltachat::{
config::Config,
context::Context,
dc_receive_imf::dc_receive_imf,
imex::{imex, ImexMode},
};
use tempfile::tempdir;
async fn recv_all_emails(context: Context) -> Context {
for i in 0..100 {
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Mr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com
From: sender@testrun.org
Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
In-Reply-To: Mr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}",
i = i,
i_dec = i - 1,
);
dc_receive_imf(&context, black_box(imf_raw.as_bytes()), false)
.await
.unwrap();
}
context
}
async fn create_context() -> Context {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 100;
let context = Context::new(dbfile.into(), id).await.unwrap();
let backup: PathBuf = std::env::current_dir()
.unwrap()
.join("delta-chat-backup.tar")
.into();
if backup.exists().await {
println!("Importing backup");
imex(&context, ImexMode::ImportBackup, &backup, None)
.await
.unwrap();
}
let addr = "alice@example.com";
context.set_config(Config::Addr, Some(addr)).await.unwrap();
context
.set_config(Config::ConfiguredAddr, Some(addr))
.await
.unwrap();
context
.set_config(Config::Configured, Some("1"))
.await
.unwrap();
context
}
fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("Receive messages");
group.bench_function("Receive 100 simple text msgs", |b| {
b.to_async(AsyncStdExecutor).iter_batched(
|| block_on(create_context()),
|context| recv_all_emails(black_box(context)),
BatchSize::LargeInput,
);
});
group.finish();
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -20,6 +20,8 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("search hello", |b| {
b.iter(|| block_on(async { search_benchmark(black_box(&path)).await }))
});
} else {
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
use crate::chat::ChatItem;
use crate::constants::{DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
use crate::constants::DC_MSG_ID_DAYMARKER;
use crate::location::Location;
use crate::message::MsgId;
@@ -18,7 +18,6 @@ impl dc_array_t {
Self::MsgIds(array) => array[index].to_u32(),
Self::Chat(array) => match array[index] {
ChatItem::Message { msg_id } => msg_id.to_u32(),
ChatItem::Marker1 => DC_MSG_ID_MARKER1,
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
},
Self::Locations(array) => array[index].location_id,
@@ -31,7 +30,6 @@ impl dc_array_t {
Self::MsgIds(_) => None,
Self::Chat(array) => array.get(index).and_then(|item| match item {
ChatItem::Message { .. } => None,
ChatItem::Marker1 { .. } => None,
ChatItem::DayMarker { timestamp } => Some(*timestamp),
}),
Self::Locations(array) => array.get(index).map(|location| location.timestamp),

View File

@@ -31,13 +31,13 @@ use rand::Rng;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, Origin};
use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateId;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
@@ -458,7 +458,37 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
}
let event = &*event;
event.as_id()
match event.typ {
EventType::Info(_) => 100,
EventType::SmtpConnected(_) => 101,
EventType::ImapConnected(_) => 102,
EventType::SmtpMessageSent(_) => 103,
EventType::ImapMessageDeleted(_) => 104,
EventType::ImapMessageMoved(_) => 105,
EventType::NewBlobFile(_) => 150,
EventType::DeletedBlobFile(_) => 151,
EventType::Warning(_) => 300,
EventType::Error(_) => 400,
EventType::ErrorSelfNotInGroup(_) => 410,
EventType::MsgsChanged { .. } => 2000,
EventType::IncomingMsg { .. } => 2005,
EventType::MsgsNoticed { .. } => 2008,
EventType::MsgDelivered { .. } => 2010,
EventType::MsgFailed { .. } => 2012,
EventType::MsgRead { .. } => 2015,
EventType::ChatModified(_) => 2020,
EventType::ChatEphemeralTimerModified { .. } => 2021,
EventType::ContactsChanged(_) => 2030,
EventType::LocationChanged(_) => 2035,
EventType::ConfigureProgress { .. } => 2041,
EventType::ImexProgress(_) => 2051,
EventType::ImexFileWritten(_) => 2052,
EventType::SecurejoinInviterProgress { .. } => 2060,
EventType::SecurejoinJoinerProgress { .. } => 2061,
EventType::ConnectivityChanged => 2100,
EventType::SelfavatarChanged => 2110,
EventType::WebxdcStatusUpdate { .. } => 2120,
}
}
#[no_mangle]
@@ -493,14 +523,16 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
let id = id.unwrap_or_default();
id as libc::c_int
id.to_u32() as libc::c_int
}
EventType::ConfigureProgress { progress, .. } | EventType::ImexProgress(progress) => {
*progress as libc::c_int
}
EventType::ImexFileWritten(_) => 0,
EventType::SecurejoinInviterProgress { contact_id, .. }
| EventType::SecurejoinJoinerProgress { contact_id, .. } => *contact_id as libc::c_int,
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
contact_id.to_u32() as libc::c_int
}
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
}
}
@@ -533,8 +565,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ImexFileWritten(_)
| EventType::MsgsNoticed(_)
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::ChatModified(_) => 0,
| EventType::SelfavatarChanged => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
@@ -544,8 +576,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
EventType::WebxdcStatusUpdate {
status_update_id, ..
} => status_update_id.to_u32() as libc::c_int,
status_update_serial,
..
} => status_update_serial.to_u32() as libc::c_int,
}
}
@@ -654,7 +687,7 @@ pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *
#[no_mangle]
pub unsafe extern "C" fn dc_stop_io(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_shutdown()");
eprintln!("ignoring careless call to dc_stop_io()");
return;
}
let ctx = &*context;
@@ -717,7 +750,11 @@ pub unsafe extern "C" fn dc_get_chatlist(
let ctx = &*context;
let qs = to_opt_string_lossy(query_str);
let qi = if query_id == 0 { None } else { Some(query_id) };
let qi = if query_id == 0 {
None
} else {
Some(ContactId::new(query_id))
};
block_on(async move {
match chatlist::Chatlist::try_load(ctx, flags as usize, qs.as_deref(), qi)
@@ -745,7 +782,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
let ctx = &*context;
block_on(async move {
ChatId::create_for_contact(ctx, contact_id)
ChatId::create_for_contact(ctx, ContactId::new(contact_id))
.await
.log_err(ctx, "Failed to create chat from contact_id")
.map(|id| id.to_u32())
@@ -765,7 +802,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
let ctx = &*context;
block_on(async move {
ChatId::lookup_by_contact(ctx, contact_id)
ChatId::lookup_by_contact(ctx, ContactId::new(contact_id))
.await
.log_err(ctx, "Failed to get chat for contact_id")
.unwrap_or_default() // unwraps the Result
@@ -903,7 +940,7 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
pub unsafe extern "C" fn dc_get_webxdc_status_updates(
context: *mut dc_context_t,
msg_id: u32,
status_update_id: u32,
last_known_serial: u32,
) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_webxdc_status_updates()");
@@ -913,11 +950,7 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
block_on(ctx.get_webxdc_status_updates(
MsgId::new(msg_id),
if status_update_id == 0 {
None
} else {
Some(StatusUpdateId::new(status_update_id))
},
StatusUpdateSerial::new(last_known_serial),
))
.unwrap_or_else(|_| "".to_string())
.strdup()
@@ -1024,22 +1057,17 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
context: *mut dc_context_t,
chat_id: u32,
flags: u32,
marker1before: u32,
_marker1before: u32,
) -> *mut dc_array::dc_array_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_chat_msgs()");
return ptr::null_mut();
}
let ctx = &*context;
let marker_flag = if marker1before <= DC_MSG_ID_LAST_SPECIAL {
None
} else {
Some(MsgId::new(marker1before))
};
block_on(async move {
Box::into_raw(Box::new(
chat::get_chat_msgs(ctx, ChatId::new(chat_id), flags, marker_flag)
chat::get_chat_msgs(ctx, ChatId::new(chat_id), flags)
.await
.unwrap_or_log_default(ctx, "failed to get chat msgs")
.into(),
@@ -1346,7 +1374,10 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
let arr = dc_array_t::from(
chat::get_chat_contacts(ctx, ChatId::new(chat_id))
.await
.unwrap_or_log_default(ctx, "Failed get_chat_contacts"),
.unwrap_or_log_default(ctx, "Failed get_chat_contacts")
.iter()
.map(|id| id.to_u32())
.collect::<Vec<u32>>(),
);
Box::into_raw(Box::new(arr))
})
@@ -1456,7 +1487,7 @@ pub unsafe extern "C" fn dc_is_contact_in_chat(
block_on(chat::is_contact_in_chat(
ctx,
ChatId::new(chat_id),
contact_id,
ContactId::new(contact_id),
))
.log_err(ctx, "is_contact_in_chat failed")
.unwrap_or_default() as libc::c_int
@@ -1477,7 +1508,7 @@ pub unsafe extern "C" fn dc_add_contact_to_chat(
block_on(chat::add_contact_to_chat(
ctx,
ChatId::new(chat_id),
contact_id,
ContactId::new(contact_id),
))
.log_err(ctx, "Failed to add contact")
.is_ok() as libc::c_int
@@ -1498,7 +1529,7 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat(
block_on(chat::remove_contact_from_chat(
ctx,
ChatId::new(chat_id),
contact_id,
ContactId::new(contact_id),
))
.log_err(ctx, "Failed to remove contact")
.is_ok() as libc::c_int
@@ -1745,6 +1776,27 @@ pub unsafe extern "C" fn dc_forward_msgs(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_resend_msgs(
context: *mut dc_context_t,
msg_ids: *const u32,
msg_cnt: libc::c_int,
) -> libc::c_int {
if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 {
eprintln!("ignoring careless call to dc_resend_msgs()");
return 0;
}
let ctx = &*context;
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
if let Err(err) = block_on(chat::resend_msgs(ctx, &msg_ids)) {
error!(ctx, "Resending failed: {}", err);
0
} else {
1
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_markseen_msgs(
context: *mut dc_context_t,
@@ -1833,7 +1885,8 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
Contact::lookup_id_by_addr(ctx, &to_string_lossy(addr), Origin::IncomingReplyTo)
.await
.unwrap_or_log_default(ctx, "failed to lookup id")
.unwrap_or(0)
.map(|id| id.to_u32())
.unwrap_or_default()
})
}
@@ -1853,6 +1906,7 @@ pub unsafe extern "C" fn dc_create_contact(
block_on(async move {
Contact::create(ctx, &name, &to_string_lossy(addr))
.await
.map(|id| id.to_u32())
.unwrap_or(0)
})
}
@@ -1890,8 +1944,10 @@ pub unsafe extern "C" fn dc_get_contacts(
let query = to_opt_string_lossy(query);
block_on(async move {
match Contact::get_all(ctx, flags, query).await {
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(contacts))),
match Contact::get_all(ctx, flags, query.as_deref()).await {
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(
contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>(),
))),
Err(_) => ptr::null_mut(),
}
})
@@ -1928,7 +1984,10 @@ pub unsafe extern "C" fn dc_get_blocked_contacts(
Contact::get_all_blocked(ctx)
.await
.log_err(ctx, "Can't get blocked contacts")
.unwrap_or_default(),
.unwrap_or_default()
.iter()
.map(|id| id.to_u32())
.collect::<Vec<u32>>(),
)))
})
}
@@ -1939,7 +1998,8 @@ pub unsafe extern "C" fn dc_block_contact(
contact_id: u32,
block: libc::c_int,
) {
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL as u32 {
let contact_id = ContactId::new(contact_id);
if context.is_null() || contact_id.is_special() {
eprintln!("ignoring careless call to dc_block_contact()");
return;
}
@@ -1969,7 +2029,7 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo(
let ctx = &*context;
block_on(async move {
Contact::get_encrinfo(ctx, contact_id)
Contact::get_encrinfo(ctx, ContactId::new(contact_id))
.await
.map(|s| s.strdup())
.unwrap_or_else(|e| {
@@ -1984,7 +2044,8 @@ pub unsafe extern "C" fn dc_delete_contact(
context: *mut dc_context_t,
contact_id: u32,
) -> libc::c_int {
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL as u32 {
let contact_id = ContactId::new(contact_id);
if context.is_null() || contact_id.is_special() {
eprintln!("ignoring careless call to dc_delete_contact()");
return 0;
}
@@ -2010,7 +2071,7 @@ pub unsafe extern "C" fn dc_get_contact(
let ctx = &*context;
block_on(async move {
Contact::get_by_id(ctx, contact_id)
Contact::get_by_id(ctx, ContactId::new(contact_id))
.await
.map(|contact| Box::into_raw(Box::new(ContactWrapper { context, contact })))
.unwrap_or_else(|_| ptr::null_mut())
@@ -2433,7 +2494,7 @@ pub unsafe extern "C" fn dc_array_get_contact_id(
return 0;
}
(*array).get_location(index).contact_id
(*array).get_location(index).contact_id.to_u32()
}
#[no_mangle]
pub unsafe extern "C" fn dc_array_get_msg_id(
@@ -2949,7 +3010,7 @@ pub unsafe extern "C" fn dc_msg_get_from_id(msg: *mut dc_msg_t) -> u32 {
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_from_id()
ffi_msg.message.get_from_id().to_u32()
}
#[no_mangle]
@@ -3660,7 +3721,7 @@ pub unsafe extern "C" fn dc_contact_get_id(contact: *mut dc_contact_t) -> u32 {
return 0;
}
let ffi_contact = &*contact;
ffi_contact.contact.get_id()
ffi_contact.contact.get_id().to_u32()
}
#[no_mangle]
@@ -3937,6 +3998,25 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
}
let addr = to_string_lossy(addr);
let ctx = &*context;
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
Some(provider) => provider,
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
context: *const dc_context_t,
addr: *const libc::c_char,
) -> *const dc_provider_t {
if context.is_null() || addr.is_null() {
eprintln!("ignoring careless call to dc_provider_new_from_email_with_dns()");
return ptr::null();
}
let addr = to_string_lossy(addr);
let ctx = &*context;
let socks5_enabled = block_on(async move {
ctx.get_config_bool(config::Config::Socks5Enabled)

View File

@@ -111,19 +111,19 @@ impl Lot {
match self {
Self::Summary(_) => Default::default(),
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { contact_id, .. } => *contact_id,
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::AskVerifyGroup { .. } => Default::default(),
Qr::FprOk { contact_id } => *contact_id,
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default(),
Qr::FprOk { contact_id } => contact_id.to_u32(),
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Addr { contact_id } => *contact_id,
Qr::Addr { contact_id } => contact_id.to_u32(),
Qr::Url { .. } => Default::default(),
Qr::Text { .. } => Default::default(),
Qr::WithdrawVerifyContact { contact_id, .. } => *contact_id,
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::WithdrawVerifyGroup { .. } => Default::default(),
Qr::ReviveVerifyContact { contact_id, .. } => *contact_id,
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::ReviveVerifyGroup { .. } => Default::default(),
},
Self::Error(_) => Default::default(),

99
draft/aeap-mvp.md Normal file
View File

@@ -0,0 +1,99 @@
AEAP MVP
========
Changes to the UIs
------------------
- The secondary self addresses (see below) are shown in the UI, but not editable.
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
Changes in the core
-------------------
- DONE: We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
- DONE: If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
- The key stays the same.
- No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
- When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
- When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
AND there is a `Chat-Version` header\
AND the message timestamp is newer than the contact's `lastseen` (to prevent changing the address back when messages arrive out of order) (this condition is not that important since we will have eventual consistency even without it):
Replace the contact in _all_ groups, possibly deduplicate the members list, and add a system message to all of these chats.
- Note that we can't simply compare the keys byte-by-byte, since the UID may have changed, or the sender may have rotated the key and signed the new key with the old one.
### Notes:
- We treat protected and non-protected chats the same
- We leave the aeap transition statement away since it seems not to be needed, makes things harder on the sending side, wastes some network traffic, and is worse for privacy (since more pepole know what old addresses you had).
- As soon as we encrypt read receipts, sending a read receipt will be enough to tell a lot of people that you transitioned
- AEAP will make the problem of inconsistent group state worse, both because it doesn't work if the message is unencrypted (even if the design allowed it, it would be problematic security-wise) and because some chat partners may have gotten the transition and some not. We should do something against this at some point in the future, like asking the user whether they want to add/remove the members to restore consistent group state.
#### Downsides of this design:
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
#### Upsides:
- With this approach, it's easy to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
- Faster transation: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.
### Alternatives and old discussions/plans:
- Change the contact instead of rewriting the group member lists. This seems to call for more trouble since we will end up with multiple contacts having the same email address.
- If needed, we could add a header a) indicating that the sender did an address transition or b) listing all the secondary (old) addresses. For now, there is no big enough benefit to warrant introducing another header and its processing on the receiver side (including all the neccessary checks and handling of error cases). Instead, we only check for the `Chat-Version` header to prevent accidental transitions when an MUA user sends a message from another email address with the same key.
- The condition for a transition temporarily was:
> When receiving a message: If we are going to assign a message to a chat, but the sender is not a member of this chat\
> AND the signing key is the same as the direct (non-gossiped) key of one of the chat members\
> AND ...
However, this would mean that in 1:1 messages can't trigger a transition, since we don't assign private messages to the parent chat, but always to the 1:1 chat with the sender.
<details>
<summary>Some previous state of the discussion, which temporarily lived in an issue description</summary>
Summarizing the discussions from https://github.com/deltachat/deltachat-core-rust/pull/2896, mostly quoting @hpk42:
1. (DONE) At the time of configure we push the current primary to become a secondary.
2. When a message is sent out to a chat, and the message is encrypted, and we have secondary addresses, then we
a) add a protected "AEAP-Replacement" header that contains all secondary addresses
b) if any of the secondary addresses is in the chat's member list, we remove it and leave a system message that we did so
3. When an encrypted message with a replacement header is received, replace the e-mail address of all secondary contacts (if they exist) with the new primary and drop a sysmessage in all chats the secondary is member off. This might (in edge cases) result in chats that have two or more contacts with the same e-mail address. We might ignore this for a first release and just log a warning. Let's maybe not get hung up on this case before everything else works.
Notes:
- for now we will send out aeap replacement headers forever, there is no termination condition other than lack of secondary addresses. I think that's fine for now. Later on we might introduce options to remove secondary addresses but i wouldn't do this for a first release/PR.
- the design is resilient against changing e-mail providers from A to B to C and then back to A, with partially updated chats and diverging views from recipients/contacts on this transition. In the end, you will have a primary and some secondaries, and when you start sending out messages everybody will eventually synchronize when they receive the current state of primaries/secondaries.
- of course on incoming message for need to check for each stated secondary address in the replacement header that it uses the same signature as the signature we verified as valid with the incoming message **--> Also we have to somehow make sure that the signing key was not just gossiped from some random other person in some group.**
- there are no extra flags/columns in the database needed (i hope)
#### Downsides of the chosen approach:
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
- There will be multiple contacts with the same address in the database. We will have to do something against this at some point.
The most obvious alternative would be to create a new contact with the new address and replace the old contact in the groups.
#### Upsides:
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
- (Also, less important: Slightly faster transation: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.)
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
_end of the previous state of the discussion_
</details>
Other
-----
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.

View File

@@ -1,33 +0,0 @@
AEAP MVP
========
Changes to the UIs
------------------
- The secondary self addresses (see below) are shown in the UI, but not editable.
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
Changes in the core
-------------------
- We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
- If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
- The key stays the same.
- No changes for 1:1 chats, there simply is a new one
- When we send a message to a group, and the primary address is not a member of a group, but a secondary address is:
Add Chat-Group-Member-Removed=<old address> and Chat-Group-Member-Added=<new address> headers to this message
- On the receiving side, make sure that we accept this (even in verified groups) if the message is signed and the key stayed the same
Other
-----
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.

View File

@@ -1,12 +1,14 @@
# Webxdc Developer Reference
(This document may eventually be merged with the [webxdc guidebook](https://deltachat.github.io/webxdc_docs/), where you may currently find other useful information.)
## Webxdc File Format
- a **Webxdc app** is a **ZIP-file** with the extension `.xdc`
- a **Webxdc** is a **ZIP-file** with the extension `.xdc`
- the ZIP-file must use the default compression methods as of RFC 1950,
this is "Deflate" or "Store"
- the ZIP-file must contain at least the file `index.html`
- if the Webxdc app is started, `index.html` is opened in a restricted webview
- if the Webxdc is started, `index.html` is opened in a restricted webview
that allow accessing resources only from the ZIP-file
@@ -26,16 +28,18 @@ no need to add `webxdc.js` to your ZIP-file):
window.webxdc.sendUpdate(update, descr);
```
Webxdc apps are usually shared in a chat and run independently on each peer.
A Webxdc is usually shared in a chat and run independently on each peer.
To get a shared state, the peers use `sendUpdate()` to send updates to each other.
- `update`: an object with the following fields:
- `update`: an object with the following properties:
- `update.payload`: any javascript primitive, array or object.
- `update.info`: optional, short, informational message that will be added to the chat,
eg. "Alice voted" or "Bob scored 123 in MyGame";
usually only one line of text is shown,
use this option sparingly to not spam the chat.
- `update.summary`: optional, short text, shown beside app icon;
- `update.document`: optional, name of the document in edit,
must not be used eg. in games where the Webxdc does not create documents
- `update.summary`: optional, short text, shown beside Webxdc icon;
it is recommended to use some aggregated value, eg. "8 votes", "Highscore: 123"
- `descr`: short, human-readable description what this update is about.
@@ -45,48 +49,41 @@ All peers, including the sending one,
will receive the update by the callback given to `setUpdateListener()`.
There are situations where the user cannot send messages to a chat,
eg. contact requests or if the user has left a group.
eg. if the webxdc instance comes as a contact request or if the user has left a group.
In these cases, you can still call `sendUpdate()`,
however, the update won't be sent to other peers
and you won't get the update by `setUpdateListener()` nor by `getAllUpdates()`.
and you won't get the update by `setUpdateListener()`.
### setUpdateListener()
```js
window.webxdc.setUpdateListener((update) => {});
let promise = window.webxdc.setUpdateListener((update) => {}, serial);
```
With `setUpdateListener()` you define a callback that receives the updates
sent by `sendUpdate()`.
sent by `sendUpdate()`. The callback is called for updates sent by you or other peers.
The `serial` specifies the last serial that you know about (defaults to 0).
The returned promise resolves when the listener has processed all the update messages known at the time when `setUpdateListener` was called.
- `update`: passed to the callback on updates with the following fields:
`update.payload`: equals the payload given to `sendUpdate()`
Each `update` which is passed to the callback comes with the following properties:
The callback is called for updates sent by you or other peers.
- `update.payload`: equals the payload given to `sendUpdate()`
- `update.serial`: the serial number of this update.
Serials are larger `0` and newer serials have higher numbers.
There may be gaps in the serials
and it is not guaranteed that the next serial is exactly incremented by one.
### getAllUpdates()
- `update.max_serial`: the maximum serial currently known.
If `max_serial` equals `serial` this update is the last update (until new network messages arrive).
```js
updates = await window.webxdc.getAllUpdates();
```
- `update.info`: optional, short, informational message (see `sendUpdate()`)
In case your Webxdc was just started,
you may want to reconstruct the state from the last run -
and also incorporate updates that may have arrived while the app was not running.
- `update.document`: optional, document name as set by the sender, (see `sendUpdate()`),
implementations show the document name eg. beside the app icon or in the title bar
- `updates`: All previous updates in an array,
eg. `[{payload: "foo"},{payload: "bar"}]`
if `webxdc.sendUpdate({payload: "foo"}); webxdc.sendUpdate({payload: "bar"};` was called on the last run.
The updates are wrapped into a Promise that you can `await` for.
If you are not in an async function and cannot use `await` therefore,
you can get the updates with `then()`:
```js
window.webxdc.getAllUpdates().then(updates => {});
```
- `update.summary`: optional, short text, shown beside icon (see `sendUpdate()`)
### selfAddr
@@ -120,17 +117,21 @@ some basic information are read and used from there.
the `manifest.toml` has the following format
```toml
name = "My App Name"
name = "My Name"
source_code_url = "https://example.org/orga/repo"
```
- **name** - The name of the app.
If no name is set or if there is no manifest, the filename is used as the app name.
- `name` - The name of the Webxdc.
If no name is set or if there is no manifest, the filename is used as the Webxdc name.
- `source_code_url` - Optional URL where the source code of the Webxdc and maybe other information can be found.
UI may make the url accessible via a "Help" menu in the Webxdc window.
## App Icon
## Webxdc Icon
If the ZIP-root contains an `icon.png` or `icon.jpg`,
these files are used as the icon for the app.
these files are used as the icon for the Webxdc.
The icon should be a square at reasonable width/height;
round corners etc. will be added by the implementations as needed.
If no icon is set, a default icon will be used.
@@ -162,9 +163,7 @@ The following example shows an input field and every input is show on all peers
document.getElementById('output').innerHTML += update.payload + "<br>";
}
window.webxdc.setUpdateListener(receiveUpdate);
window.webxdc.getAllUpdates().then(updates => updates.forEach(receiveUpdate));
window.webxdc.setUpdateListener(receiveUpdate, 0);
</script>
</body>
</html>
@@ -172,7 +171,7 @@ The following example shows an input field and every input is show on all peers
[Webxdc Development Tool](https://github.com/deltachat/webxdc-dev)
offers an **Webxdc Simulator** that can be used in many browsers without any installation needed.
You can also use that repository as a template for your own app -
You can also use that repository as a template for your own Webxdc -
just clone and start adapting things to your need.
@@ -182,13 +181,18 @@ just clone and start adapting things to your need.
- [Draw](https://github.com/adbenitez/draw.xdc)
- [Poll](https://github.com/r10s/webxdc-poll/)
- [Tic Tac Toe](https://github.com/Simon-Laux/tictactoe.xdc)
- Even more with [Topic #webxdc on Github](https://github.com/topics/webxdc)
- Even more with [Topic #webxdc on Github](https://github.com/topics/webxdc) or in the [webxdc GitHub organization](https://github.com/webxdc)
## Closing Remarks
- older devices might not have the newest js features in their webview,
you may want to transpile your code down to an older js version eg. with https://babeljs.io
- viewport and scaling features are implementation specific,
if you want to have an explicit behavior, you can add eg.
`<meta name="viewport" content="initial-scale=1; user-scalable=no">` to your Webxdc
- the `<title>` tag should not be used and its content is usually not displayed;
instead, use the `name` property from `manifest.toml`
- there are tons of ideas for enhancements of the API and the file format,
eg. in the future, we will may define icon- and manifest-files,
allow to aggregate the state or add metadata.
allow to aggregate the state or add metadata.

View File

@@ -17,11 +17,10 @@ use deltachat::download::DownloadState;
use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId};
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::EventType;
use deltachat::{config, provider};
use std::fs;
use std::time::{Duration, SystemTime};
@@ -84,6 +83,7 @@ async fn reset_tables(context: &Context, bits: i32) {
)
.await
.unwrap();
context.sql().config_cache().write().await.clear();
context
.sql()
.execute("DELETE FROM leftgrps;", paramsv![])
@@ -92,16 +92,13 @@ async fn reset_tables(context: &Context, bits: i32) {
println!("(8) Rest but server config reset.");
}
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
context.emit_msgs_changed_without_ids();
}
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
let data = dc_read_file(context, filename).await?;
if let Err(err) = dc_receive_imf(context, &data, "import", false).await {
if let Err(err) = dc_receive_imf(context, &data, false).await {
println!("dc_receive_imf errored: {:?}", err);
}
Ok(())
@@ -163,10 +160,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
if read_cnt > 0 {
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
context.emit_msgs_changed_without_ids();
}
true
}
@@ -209,7 +203,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 {
if msg.get_from_id() == ContactId::SELF {
""
} else if msg.get_state() == MessageState::InSeen {
"[SEEN]"
@@ -267,9 +261,8 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
Ok(())
}
async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
for contact_id in contacts {
let line;
let mut line2 = "".to_string();
let contact = Contact::get_by_id(context, *contact_id).await?;
let name = contact.get_display_name();
@@ -284,24 +277,20 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
} else {
""
};
line = format!(
let line = format!(
"{}{} <{}>",
if !name.is_empty() {
&name
name
} else {
"<name unset>"
},
verified_str,
if !addr.is_empty() {
&addr
} else {
"addr unset"
}
if !addr.is_empty() { addr } else { "addr unset" }
);
let peerstate = Peerstate::from_addr(context, &addr)
let peerstate = Peerstate::from_addr(context, addr)
.await
.expect("peerstate error");
if peerstate.is_some() && *contact_id != 1 {
if peerstate.is_some() && *contact_id != ContactId::SELF {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
@@ -410,6 +399,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
html <msg-id>\n\
listfresh\n\
forward <msg-id> <chat-id>\n\
resend <msg-id>\n\
markseen <msg-id>\n\
delmsg <msg-id>\n\
===========================Contact commands==\n\
@@ -430,7 +420,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
joinqr <qr-content>\n\
setqr <qr-content>\n\
providerinfo <addr>\n\
event <event-id to test>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
clear -- clear screen\n\
@@ -650,14 +639,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let time_start = std::time::SystemTime::now();
let msglist =
chat::get_chat_msgs(&context, sel_chat.get_id(), DC_GCM_ADDDAYMARKER, None).await?;
chat::get_chat_msgs(&context, sel_chat.get_id(), DC_GCM_ADDDAYMARKER).await?;
let time_needed = time_start.elapsed().unwrap_or_default();
let msglist: Vec<MsgId> = msglist
.into_iter()
.map(|x| match x {
ChatItem::Message { msg_id } => msg_id,
ChatItem::Marker1 => MsgId::new(DC_MSG_ID_MARKER1),
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
})
.collect();
@@ -719,7 +707,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"createchat" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id: u32 = arg1.parse()?;
let contact_id = ContactId::new(arg1.parse()?);
let chat_id = ChatId::create_for_contact(&context, contact_id).await?;
println!("Single#{} created successfully.", chat_id,);
@@ -747,7 +735,7 @@ 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: u32 = arg1.parse()?;
let contact_id_0 = ContactId::new(arg1.parse()?);
chat::add_contact_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), contact_id_0)
.await?;
println!("Contact added to chat.");
@@ -755,7 +743,7 @@ 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: u32 = arg1.parse()?;
let contact_id_1 = ContactId::new(arg1.parse()?);
chat::remove_contact_from_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
@@ -771,7 +759,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::set_chat_name(
&context,
sel_chat.as_ref().unwrap().get_id(),
&format!("{} {}", arg1, arg2).trim(),
format!("{} {}", arg1, arg2).trim(),
)
.await?;
@@ -937,13 +925,24 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"listmsgs" => {
ensure!(!arg1.is_empty(), "Argument <query> missing.");
let query = format!("{} {}", arg1, arg2).trim().to_string();
let chat = sel_chat.as_ref().map(|sel_chat| sel_chat.get_id());
let time_start = std::time::SystemTime::now();
let msglist = context.search_msgs(chat, arg1).await?;
let msglist = context.search_msgs(chat, &query).await?;
let time_needed = time_start.elapsed().unwrap_or_default();
log_msglist(&context, &msglist).await?;
println!("{} messages.", msglist.len());
println!(
"{}{} messages for {}search of \"{}\"",
msglist.len(),
if msglist.len() == 1000 { "+" } else { "" },
if chat.is_none() {
"global "
} else {
"in-chat-"
},
query,
);
println!("{:?} to create this list", time_needed);
}
"draft" => {
@@ -1100,6 +1099,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
msg_ids[0] = MsgId::new(arg1.parse()?);
chat::forward_msgs(&context, &msg_ids, chat_id).await?;
}
"resend" => {
ensure!(!arg1.is_empty(), "Arguments <msg-id> expected");
let mut msg_ids = [MsgId::new(0); 1];
msg_ids[0] = MsgId::new(arg1.parse()?);
chat::resend_msgs(&context, &msg_ids).await?;
}
"markseen" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = vec![MsgId::new(0)];
@@ -1139,7 +1145,7 @@ 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: u32 = arg1.parse()?;
let contact_id = ContactId::new(arg1.parse()?);
let contact = Contact::get_by_id(&context, contact_id).await?;
let name_n_addr = contact.get_name_n_addr();
@@ -1174,16 +1180,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"delcontact" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
Contact::delete(&context, arg1.parse()?).await?;
Contact::delete(&context, ContactId::new(arg1.parse()?)).await?;
}
"block" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
let contact_id = ContactId::new(arg1.parse()?);
Contact::block(&context, contact_id).await?;
}
"unblock" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
let contact_id = ContactId::new(arg1.parse()?);
Contact::unblock(&context, contact_id).await?;
}
"listblocked" => {
@@ -1224,17 +1230,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
}
}
// TODO: implement this again, unclear how to match this through though, without writing a parser.
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
// let event = arg1.parse()?;
// let event = EventType::from_u32(event).ok_or(format_err!("EventType::from_u32({})", event))?;
// 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,
// );
// }
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");

View File

@@ -207,11 +207,12 @@ const CHAT_COMMANDS: [&str; 36] = [
"accept",
"blockchat",
];
const MESSAGE_COMMANDS: [&str; 7] = [
const MESSAGE_COMMANDS: [&str; 8] = [
"listmsgs",
"msginfo",
"listfresh",
"forward",
"resend",
"markseen",
"delmsg",
"download",
@@ -227,13 +228,12 @@ const CONTACT_COMMANDS: [&str; 9] = [
"unblock",
"listblocked",
];
const MISC_COMMANDS: [&str; 12] = [
const MISC_COMMANDS: [&str; 11] = [
"getqr",
"getqrsvg",
"getbadqr",
"checkqr",
"joinqr",
"event",
"fileinfo",
"clear",
"exit",
@@ -416,7 +416,7 @@ async fn handle_cmd(
}
"getqr" | "getbadqr" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
@@ -433,7 +433,7 @@ async fn handle_cmd(
}
"getqrsvg" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
match get_securejoin_qr_svg(&ctx, group).await {
Ok(svg) => {

6
node/.prettierrc.yml Normal file
View File

@@ -0,0 +1,6 @@
# .prettierrc
trailingComma: es5
tabWidth: 2
semi: false
singleQuote: true
jsxSingleQuote: true

21
node/CONTRIBUTORS.md Normal file
View File

@@ -0,0 +1,21 @@
# Contributors
| Name | GitHub |
| :-------------------- | :----------------------------------------------- |
| **Lars-Magnus Skog** | |
| **jikstra** | |
| **Simon Laux** | [**@Simon-Laux**](https://github.com/Simon-Laux) |
| **Jikstra** | [**@Jikstra**](https://github.com/Jikstra) |
| **Nico de Haen** | |
| **B. Petersen** | |
| **Karissa McKelvey** | [**@karissa**](https://github.com/karissa) |
| **developer** | |
| **Alexander Krotov** | |
| **Floris Bruynooghe** | |
| **lefherz** | |
| **Pablo** | [**@pabzm**](https://github.com/pabzm) |
| **pabzm** | |
| **holger krekel** | |
| **Robert Schütz** | |
| **bb** | |
| **Charles Paul** | |

674
node/LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

247
node/README.md Normal file
View File

@@ -0,0 +1,247 @@
# deltachat-node
> node.js bindings for [`deltachat-core-rust`](..)
[![npm](https://img.shields.io/npm/v/deltachat-node.svg)](https://www.npmjs.com/package/deltachat-node)
![Node version](https://img.shields.io/node/v/deltachat-node.svg)
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io)
`deltachat-node` primarily aims to offer two things:
- A high level JavaScript api with syntactic sugar
- A low level c binding api around [`deltachat-core-rust`](..)
This code used to live at [`deltachat-node`](https://github.com/deltachat/deltachat-node)
## Table of Contents
<details><summary>Click to expand</summary>
- [Install](#install)
- [Dependencies](#dependencies)
- [Build from source](#build-from-source)
- [Usage](#usage)
- [Developing](#developing)
- [License](#license)
</details>
## Install
By default the installation will build try to use the bundled prebuilds in the
npm package. If this fails it falls back to compile `../deltachat-core-rust` from
this repository, using `scripts/rebuild-core.js`.
To install from npm use:
```
npm install deltachat-node
```
## Dependencies
- Nodejs >= `v16.0.0`
- rustup (optional if you can't use the prebuilds)
> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
## Build from source
If you want to build from source, make sure that you have `rustup` installed.
You can either use `npm install deltachat-node --build-from-source` to force
building from source or clone this repository and follow this steps:
1. `git clone https://github.com/deltachat/deltachat-core-rust.git`
2. `cd deltachat-core-rust`
3. `npm i`
4. `npm run build`
> Our `package.json` file is located in the root directory of this repository,
> not inside this folder. (We need this in order to include the rust source
> code in the npm package.)
### Use build-from-source in deltachat-desktop
If you want to use the manually built node bindings in the desktop client (for
example), you can follow these instructions:
First clone the
[deltachat-desktop](https://github.com/deltachat/deltachat-desktop) repository,
e.g. with `git clone https://github.com/deltachat/deltachat-desktop`.
Then you need to make sure that this directory is referenced correctly in
deltachat-desktop's package.json. You need to change
`deltachat-desktop/package.json` like this:
```
diff --git i/package.json w/package.json
index 45893894..5154512c 100644
--- i/package.json
+++ w/package.json
@@ -83,7 +83,7 @@
"application-config": "^1.0.1",
"classnames": "^2.3.1",
"debounce": "^1.2.0",
- "deltachat-node": "1.79.3",
+ "deltachat-node": "file:../deltachat-core-rust/",
"emoji-js-clean": "^4.0.0",
"emoji-mart": "^3.0.1",
"emoji-regex": "^9.2.2",
```
Then, in the `deltachat-desktop` repository, run:
1. `npm i`
2. `npm run build`
3. And `npm run start` to start the newly built client.
### Workaround to build for x86_64 on Apple's M1
deltachat doesn't support universal (fat) binaries (that contain builds for both cpu architectures) yet, until it does you can use the following workaround to get x86_64 builds:
```
$ fnm install 17 --arch x64
$ fnm use 17
$ node -p process.arch
# result should be x64
$ cd deltachat-core-rust && rustup target add x86_64-apple-darwin && cd -
$ git apply patches/m1_build_use_x86_64.patch
$ CARGO_BUILD_TARGET=x86_64-apple-darwin npm run build
$ npm run test
```
(when using [fnm](https://github.com/Schniz/fnm) instead of nvm, you can select the architecture)
If your node and electron are already build for arm64 you can also try bulding for arm:
```
$ fnm install 16 --arch arm64
$ fnm use 16
$ node -p process.arch
# result should be arm64
$ npm_config_arch=arm64 npm run build
$ npm run test
```
## Usage
```js
const { Context } = require('deltachat-node')
const opts = {
addr: '[email]',
mail_pw: '[password]',
}
const contact = '[email]'
async function main() {
const dc = Context.open('./')
dc.on('ALL', console.log.bind(null, 'core |'))
try {
await dc.configure(opts)
} catch (err) {
console.error('Failed to configure because of: ', err)
dc.unref()
return
}
dc.startIO()
console.log('fully configured')
const contactId = dc.createContact('Test', contact)
const chatId = dc.createChatByContactId(contactId)
dc.sendMessage(chatId, 'Hi!')
console.log('sent message')
dc.once('DC_EVENT_SMTP_MESSAGE_SENT', async () => {
console.log('Message sent, shutting down...')
dc.stopIO()
console.log('stopped io')
dc.unref()
})
}
main()
```
this example can also be found in the examples folder [examples/send_message.js](./examples/send_message.js)
### Generating Docs
We are curently migrating to automaticaly generated documentation.
You can find the old documentation at [old_docs](./old_docs).
to generate the documentation, run:
```
npx typedoc
```
The resulting documentation can be found in the `docs/` folder.
An online version can be found under [js.delta.chat](https://js.delta.chat).
## Developing
### Tests and Coverage
Running `npm test` ends with showing a code coverage report, which is produced by [`nyc`](https://github.com/istanbuljs/nyc#readme).
![test output](images/tests.png)
The coverage report from `nyc` in the console is rather limited. To get a more detailed coverage report you can run `npm run coverage-html-report`. This will produce a html report from the `nyc` data and display it in a browser on your local machine.
To run the integration tests you need to set the `DCC_NEW_TMP_EMAIL` environment variables. E.g.:
```
$ export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=[token]
$ npm run test
```
### Scripts
We have the following scripts for building, testing and coverage:
- `npm run coverage` Creates a coverage report and passes it to `coveralls`. Only done by `Travis`.
- `npm run coverage-html-report` Generates a html report from the coverage data and opens it in a browser on the local machine.
- `npm run generate-constants` Generates `constants.js` and `events.js` based on the `deltachat-core-rust/deltachat-ffi/deltachat.h` header file.
- `npm install` After dependencies are installed, runs `node-gyp-build` to see if the native code needs to be rebuilt.
- `npm run build` Rebuilds all code.
- `npm run build:core` Rebuilds code in `deltachat-core-rust`.
- `npm run build:bindings` Rebuilds the bindings and links with `deltachat-core-rust`.
- `ǹpm run clean` Removes all built code
- `npm run prebuildify` Builds prebuilt binary to `prebuilds/$PLATFORM-$ARCH`. Copies `deltachat.dll` from `deltachat-core-rust` for windows.
- `npm run download-prebuilds` Downloads all prebuilt binaries from github before `npm publish`.
- `npm test` Runs `standard` and then the tests in `test/index.js`.
- `npm run test-integration` Runs the integration tests.
- `npm run hallmark` Runs `hallmark` on all markdown files.
### Releases
The following steps are needed to make a release:
1. Wait until `pack-module` github action is completed
2. Run `npm publish https://download.delta.chat/node/deltachat-node-v1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
## License
Licensed under `GPL-3.0-or-later`, see [LICENSE](./LICENSE) file for details.
> Copyright © 2018 `DeltaChat` contributors.
>
> This program is free software: you can redistribute it and/or modify
> it under the terms of the GNU General Public License as published by
> the Free Software Foundation, either version 3 of the License, or
> (at your option) any later version.
>
> This program is distributed in the hope that it will be useful,
> but WITHOUT ANY WARRANTY; without even the implied warranty of
> MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> GNU General Public License for more details.
>
> You should have received a copy of the GNU General Public License
> along with this program. If not, see <http://www.gnu.org/licenses/>.
[appveyor-shield]: https://ci.appveyor.com/api/projects/status/t0narp672wpbl6pd?svg=true
[appveyor]: https://ci.appveyor.com/project/ralphtheninja/deltachat-node-d4bf8

78
node/binding.gyp Normal file
View File

@@ -0,0 +1,78 @@
{
# documentation about the format of this file can be found under https://gyp.gsrc.io/docs/InputFormatReference.md
# Variables can be specified when calling node-gyp as so:
# node-gyp configure -- -Dvarname=value
"variables": {
# Whether to use a system-wide installation of deltachat-core
# using pkg-config. Set to either "true" or "false".
"USE_SYSTEM_LIBDELTACHAT%": "<!(echo $USE_SYSTEM_LIBDELTACHAT)",
},
"targets": [
{
"target_name": "deltachat",
"sources": ["./src/module.c"],
"include_dirs": ["<!(node -e \"require('napi-macros')\")"],
"conditions": [
[
"OS == 'win'",
{
"include_dirs": ["../deltachat-ffi"],
"libraries": [
"../../target/release/deltachat.dll.lib",
],
"conditions": [
[
"USE_SYSTEM_LIBDELTACHAT == 'true'",
{
"cflags": ["<!(pkg-config --cflags deltachat)"],
"libraries": ["<!(pkg-config --libs deltachat)"],
},
],
],
},
],
[
"OS == 'linux' or OS == 'mac'",
{
"libraries": ["-lpthread"],
"cflags": ["-std=gnu99"],
"conditions": [
[
"USE_SYSTEM_LIBDELTACHAT != 'true'",
{
"include_dirs": ["../deltachat-ffi"],
"ldflags": ["-Wl,-Bsymbolic"], # Prevent sqlite3 from electron from overriding sqlcipher
"libraries": [
"../../target/release/libdeltachat.a",
"-ldl",
],
"conditions": [],
},
{
# USE_SYSTEM_LIBDELTACHAT == 'true'
"cflags": ["<!(pkg-config --cflags deltachat)"],
"libraries": ["<!(pkg-config --libs deltachat)"],
},
],
[
"OS == 'mac'",
{
"libraries": [
"-framework CoreFoundation",
"-framework CoreServices",
"-framework Security",
"-lresolv",
],
},
{
# OS == 'linux'
"libraries": ["-lm", "-lrt"],
},
],
],
},
],
],
},
],
}

1
node/binding.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require('node-gyp-build')(__dirname)

226
node/constants.js Normal file
View File

@@ -0,0 +1,226 @@
// Generated!
module.exports = {
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES: 3,
DC_CERTCK_AUTO: 0,
DC_CERTCK_STRICT: 1,
DC_CHAT_ID_ALLDONE_HINT: 7,
DC_CHAT_ID_ARCHIVED_LINK: 6,
DC_CHAT_ID_LAST_SPECIAL: 9,
DC_CHAT_ID_TRASH: 3,
DC_CHAT_TYPE_BROADCAST: 160,
DC_CHAT_TYPE_GROUP: 120,
DC_CHAT_TYPE_MAILINGLIST: 140,
DC_CHAT_TYPE_SINGLE: 100,
DC_CHAT_TYPE_UNDEFINED: 0,
DC_CHAT_VISIBILITY_ARCHIVED: 1,
DC_CHAT_VISIBILITY_NORMAL: 0,
DC_CHAT_VISIBILITY_PINNED: 2,
DC_CONNECTIVITY_CONNECTED: 4000,
DC_CONNECTIVITY_CONNECTING: 2000,
DC_CONNECTIVITY_NOT_CONNECTED: 1000,
DC_CONNECTIVITY_WORKING: 3000,
DC_CONTACT_ID_DEVICE: 5,
DC_CONTACT_ID_INFO: 2,
DC_CONTACT_ID_LAST_SPECIAL: 9,
DC_CONTACT_ID_SELF: 1,
DC_DOWNLOAD_AVAILABLE: 10,
DC_DOWNLOAD_DONE: 0,
DC_DOWNLOAD_FAILURE: 20,
DC_DOWNLOAD_IN_PROGRESS: 1000,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
DC_EVENT_CHAT_MODIFIED: 2020,
DC_EVENT_CONFIGURE_PROGRESS: 2041,
DC_EVENT_CONNECTIVITY_CHANGED: 2100,
DC_EVENT_CONTACTS_CHANGED: 2030,
DC_EVENT_DELETED_BLOB_FILE: 151,
DC_EVENT_ERROR: 400,
DC_EVENT_ERROR_SELF_NOT_IN_GROUP: 410,
DC_EVENT_IMAP_CONNECTED: 102,
DC_EVENT_IMAP_MESSAGE_DELETED: 104,
DC_EVENT_IMAP_MESSAGE_MOVED: 105,
DC_EVENT_IMEX_FILE_WRITTEN: 2052,
DC_EVENT_IMEX_PROGRESS: 2051,
DC_EVENT_INCOMING_MSG: 2005,
DC_EVENT_INFO: 100,
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,
DC_EVENT_MSGS_NOTICED: 2008,
DC_EVENT_MSG_DELIVERED: 2010,
DC_EVENT_MSG_FAILED: 2012,
DC_EVENT_MSG_READ: 2015,
DC_EVENT_NEW_BLOB_FILE: 150,
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
DC_EVENT_SELFAVATAR_CHANGED: 2110,
DC_EVENT_SMTP_CONNECTED: 101,
DC_EVENT_SMTP_MESSAGE_SENT: 103,
DC_EVENT_WARNING: 300,
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
DC_GCL_ADD_ALLDONE_HINT: 4,
DC_GCL_ADD_SELF: 2,
DC_GCL_ARCHIVED_ONLY: 1,
DC_GCL_FOR_FORWARDING: 8,
DC_GCL_NO_SPECIALS: 2,
DC_GCL_VERIFIED_ONLY: 1,
DC_GCM_ADDDAYMARKER: 1,
DC_GCM_INFO_ONLY: 2,
DC_IMEX_EXPORT_BACKUP: 11,
DC_IMEX_EXPORT_SELF_KEYS: 1,
DC_IMEX_IMPORT_BACKUP: 12,
DC_IMEX_IMPORT_SELF_KEYS: 2,
DC_INFO_PROTECTION_DISABLED: 12,
DC_INFO_PROTECTION_ENABLED: 11,
DC_KEY_GEN_DEFAULT: 0,
DC_KEY_GEN_ED25519: 2,
DC_KEY_GEN_RSA2048: 1,
DC_LP_AUTH_NORMAL: 4,
DC_LP_AUTH_OAUTH2: 2,
DC_MEDIA_QUALITY_BALANCED: 0,
DC_MEDIA_QUALITY_WORSE: 1,
DC_MSG_AUDIO: 40,
DC_MSG_FILE: 60,
DC_MSG_GIF: 21,
DC_MSG_ID_DAYMARKER: 9,
DC_MSG_ID_LAST_SPECIAL: 9,
DC_MSG_ID_MARKER1: 1,
DC_MSG_IMAGE: 20,
DC_MSG_STICKER: 23,
DC_MSG_TEXT: 10,
DC_MSG_VIDEO: 50,
DC_MSG_VIDEOCHAT_INVITATION: 70,
DC_MSG_VOICE: 41,
DC_MSG_WEBXDC: 80,
DC_PROVIDER_STATUS_BROKEN: 3,
DC_PROVIDER_STATUS_OK: 1,
DC_PROVIDER_STATUS_PREPARATION: 2,
DC_QR_ACCOUNT: 250,
DC_QR_ADDR: 320,
DC_QR_ASK_VERIFYCONTACT: 200,
DC_QR_ASK_VERIFYGROUP: 202,
DC_QR_ERROR: 400,
DC_QR_FPR_MISMATCH: 220,
DC_QR_FPR_OK: 210,
DC_QR_FPR_WITHOUT_ADDR: 230,
DC_QR_REVIVE_VERIFYCONTACT: 510,
DC_QR_REVIVE_VERIFYGROUP: 512,
DC_QR_TEXT: 330,
DC_QR_URL: 332,
DC_QR_WEBRTC_INSTANCE: 260,
DC_QR_WITHDRAW_VERIFYCONTACT: 500,
DC_QR_WITHDRAW_VERIFYGROUP: 502,
DC_SHOW_EMAILS_ACCEPTED_CONTACTS: 1,
DC_SHOW_EMAILS_ALL: 2,
DC_SHOW_EMAILS_OFF: 0,
DC_SOCKET_AUTO: 0,
DC_SOCKET_PLAIN: 3,
DC_SOCKET_SSL: 1,
DC_SOCKET_STARTTLS: 2,
DC_STATE_IN_FRESH: 10,
DC_STATE_IN_NOTICED: 13,
DC_STATE_IN_SEEN: 16,
DC_STATE_OUT_DELIVERED: 26,
DC_STATE_OUT_DRAFT: 19,
DC_STATE_OUT_FAILED: 24,
DC_STATE_OUT_MDN_RCVD: 28,
DC_STATE_OUT_PENDING: 20,
DC_STATE_OUT_PREPARING: 18,
DC_STATE_UNDEFINED: 0,
DC_STR_AC_SETUP_MSG_BODY: 43,
DC_STR_AC_SETUP_MSG_SUBJECT: 42,
DC_STR_ARCHIVEDCHATS: 40,
DC_STR_AUDIO: 11,
DC_STR_BAD_TIME_MSG_BODY: 85,
DC_STR_BROADCAST_LIST: 115,
DC_STR_CANNOT_LOGIN: 60,
DC_STR_CANTDECRYPT_MSG_BODY: 29,
DC_STR_CONFIGURATION_FAILED: 84,
DC_STR_CONNECTED: 107,
DC_STR_CONNTECTING: 108,
DC_STR_CONTACT_NOT_VERIFIED: 36,
DC_STR_CONTACT_SETUP_CHANGED: 37,
DC_STR_CONTACT_VERIFIED: 35,
DC_STR_DEVICE_MESSAGES: 68,
DC_STR_DEVICE_MESSAGES_HINT: 70,
DC_STR_DOWNLOAD_AVAILABILITY: 100,
DC_STR_DRAFT: 3,
DC_STR_E2E_AVAILABLE: 25,
DC_STR_E2E_PREFERRED: 34,
DC_STR_ENCRYPTEDMSG: 24,
DC_STR_ENCR_NONE: 28,
DC_STR_ENCR_TRANSP: 27,
DC_STR_EPHEMERAL_DAY: 79,
DC_STR_EPHEMERAL_DAYS: 95,
DC_STR_EPHEMERAL_DISABLED: 75,
DC_STR_EPHEMERAL_FOUR_WEEKS: 81,
DC_STR_EPHEMERAL_HOUR: 78,
DC_STR_EPHEMERAL_HOURS: 94,
DC_STR_EPHEMERAL_MINUTE: 77,
DC_STR_EPHEMERAL_MINUTES: 93,
DC_STR_EPHEMERAL_SECONDS: 76,
DC_STR_EPHEMERAL_WEEK: 80,
DC_STR_EPHEMERAL_WEEKS: 96,
DC_STR_ERROR: 112,
DC_STR_ERROR_NO_NETWORK: 87,
DC_STR_FAILED_SENDING_TO: 74,
DC_STR_FILE: 12,
DC_STR_FINGERPRINTS: 30,
DC_STR_FORWARDED: 97,
DC_STR_GIF: 23,
DC_STR_IMAGE: 9,
DC_STR_INCOMING_MESSAGES: 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY: 111,
DC_STR_LOCATION: 66,
DC_STR_MESSAGES: 114,
DC_STR_MSGACTIONBYME: 63,
DC_STR_MSGACTIONBYUSER: 62,
DC_STR_MSGADDMEMBER: 17,
DC_STR_MSGDELMEMBER: 18,
DC_STR_MSGGROUPLEFT: 19,
DC_STR_MSGGRPIMGCHANGED: 16,
DC_STR_MSGGRPIMGDELETED: 33,
DC_STR_MSGGRPNAME: 15,
DC_STR_MSGLOCATIONDISABLED: 65,
DC_STR_MSGLOCATIONENABLED: 64,
DC_STR_NOMESSAGES: 1,
DC_STR_NOT_CONNECTED: 121,
DC_STR_NOT_SUPPORTED_BY_PROVIDER: 113,
DC_STR_ONE_MOMENT: 106,
DC_STR_OUTGOING_MESSAGES: 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
DC_STR_PART_OF_TOTAL_USED: 116,
DC_STR_PROTECTION_DISABLED: 89,
DC_STR_PROTECTION_ENABLED: 88,
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,
DC_STR_REPLY_NOUN: 90,
DC_STR_SAVED_MESSAGES: 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC: 120,
DC_STR_SECURE_JOIN_REPLIES: 118,
DC_STR_SECURE_JOIN_STARTED: 117,
DC_STR_SELF: 2,
DC_STR_SELF_DELETED_MSG_BODY: 91,
DC_STR_SENDING: 110,
DC_STR_SERVER_TURNED_OFF: 92,
DC_STR_SETUP_CONTACT_QR_DESC: 119,
DC_STR_STICKER: 67,
DC_STR_STORAGE_ON_DOMAIN: 105,
DC_STR_SUBJECT_FOR_NEW_CONTACT: 73,
DC_STR_SYNC_MSG_BODY: 102,
DC_STR_SYNC_MSG_SUBJECT: 101,
DC_STR_UNKNOWN_SENDER_FOR_CHAT: 72,
DC_STR_UPDATE_REMINDER_MSG_BODY: 86,
DC_STR_UPDATING: 109,
DC_STR_VIDEO: 10,
DC_STR_VIDEOCHAT_INVITATION: 82,
DC_STR_VIDEOCHAT_INVITE_MSG_BODY: 83,
DC_STR_VOICEMESSAGE: 7,
DC_STR_WELCOME_MESSAGE: 71,
DC_TEXT1_DRAFT: 1,
DC_TEXT1_SELF: 3,
DC_TEXT1_USERNAME: 2,
DC_VIDEOCHATTYPE_BASICWEBRTC: 1,
DC_VIDEOCHATTYPE_JITSI: 2,
DC_VIDEOCHATTYPE_UNKNOWN: 0
}

34
node/events.js Normal file
View File

@@ -0,0 +1,34 @@
/* eslint-disable quotes */
// Generated!
module.exports = {
100: 'DC_EVENT_INFO',
101: 'DC_EVENT_SMTP_CONNECTED',
102: 'DC_EVENT_IMAP_CONNECTED',
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
150: 'DC_EVENT_NEW_BLOB_FILE',
151: 'DC_EVENT_DELETED_BLOB_FILE',
300: 'DC_EVENT_WARNING',
400: 'DC_EVENT_ERROR',
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',
2015: 'DC_EVENT_MSG_READ',
2020: 'DC_EVENT_CHAT_MODIFIED',
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
2030: 'DC_EVENT_CONTACTS_CHANGED',
2035: 'DC_EVENT_LOCATION_CHANGED',
2041: 'DC_EVENT_CONFIGURE_PROGRESS',
2051: 'DC_EVENT_IMEX_PROGRESS',
2052: 'DC_EVENT_IMEX_FILE_WRITTEN',
2060: 'DC_EVENT_SECUREJOIN_INVITER_PROGRESS',
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE'
}

View File

@@ -0,0 +1,40 @@
//@ts-check
const { Context } = require('../dist')
const opts = {
addr: '[email]',
mail_pw: '[password]',
}
const contact = '[email]'
async function main() {
const dc = Context.open('./')
dc.on('ALL', console.log.bind(null, 'core |'))
try {
await dc.configure(opts)
} catch (err) {
console.error('Failed to configure because of: ', err)
dc.unref()
return
}
dc.startIO()
console.log('fully configured')
const contactId = dc.createContact('Test', contact)
const chatId = dc.createChatByContactId(contactId)
dc.sendMessage(chatId, 'Hi!')
console.log('sent message')
dc.once('DC_EVENT_SMTP_MESSAGE_SENT', async () => {
console.log('Message sent, shutting down...')
dc.stopIO()
console.log('stopped io')
dc.unref()
})
}
main()

BIN
node/images/tests.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

9
node/lib/binding.ts Normal file
View File

@@ -0,0 +1,9 @@
import { join } from 'path'
/**
* bindings are not typed yet.
* if the availible function names are required they can be found inside of `../src/module.c`
*/
export const bindings: any = require('node-gyp-build')(join(__dirname, '../'))
export default bindings

106
node/lib/chat.ts Normal file
View File

@@ -0,0 +1,106 @@
/* eslint-disable camelcase */
import binding from './binding'
import rawDebug from 'debug'
const debug = rawDebug('deltachat:node:chat')
import { C } from './constants'
import { integerToHexColor } from './util'
import { ChatJSON } from './types'
interface NativeChat {}
/**
* Wrapper around dc_chat_t*
*/
export class Chat {
constructor(public dc_chat: NativeChat) {
debug('Chat constructor')
if (dc_chat === null) {
throw new Error('native chat can not be null')
}
}
getVisibility():
| C.DC_CHAT_VISIBILITY_NORMAL
| C.DC_CHAT_VISIBILITY_ARCHIVED
| C.DC_CHAT_VISIBILITY_PINNED {
return binding.dcn_chat_get_visibility(this.dc_chat)
}
get color(): string {
return integerToHexColor(binding.dcn_chat_get_color(this.dc_chat))
}
getId(): number {
return binding.dcn_chat_get_id(this.dc_chat)
}
getName(): string {
return binding.dcn_chat_get_name(this.dc_chat)
}
getProfileImage(): string {
return binding.dcn_chat_get_profile_image(this.dc_chat)
}
getType(): number {
return binding.dcn_chat_get_type(this.dc_chat)
}
isSelfTalk(): boolean {
return Boolean(binding.dcn_chat_is_self_talk(this.dc_chat))
}
isContactRequest(): boolean {
return Boolean(binding.dcn_chat_is_contact_request(this.dc_chat))
}
isUnpromoted(): boolean {
return Boolean(binding.dcn_chat_is_unpromoted(this.dc_chat))
}
isProtected(): boolean {
return Boolean(binding.dcn_chat_is_protected(this.dc_chat))
}
get canSend(): boolean {
return Boolean(binding.dcn_chat_can_send(this.dc_chat))
}
isDeviceTalk(): boolean {
return Boolean(binding.dcn_chat_is_device_talk(this.dc_chat))
}
isSingle(): boolean {
return this.getType() === C.DC_CHAT_TYPE_SINGLE
}
isGroup(): boolean {
return this.getType() === C.DC_CHAT_TYPE_GROUP
}
isMuted(): boolean {
return Boolean(binding.dcn_chat_is_muted(this.dc_chat))
}
toJson(): ChatJSON {
debug('toJson')
const visibility = this.getVisibility()
return {
archived: visibility === C.DC_CHAT_VISIBILITY_ARCHIVED,
pinned: visibility === C.DC_CHAT_VISIBILITY_PINNED,
color: this.color,
id: this.getId(),
name: this.getName(),
profileImage: this.getProfileImage(),
type: this.getType(),
isSelfTalk: this.isSelfTalk(),
isUnpromoted: this.isUnpromoted(),
isProtected: this.isProtected(),
canSend: this.canSend,
isDeviceTalk: this.isDeviceTalk(),
isContactRequest: this.isContactRequest(),
muted: this.isMuted(),
}
}
}

42
node/lib/chatlist.ts Normal file
View File

@@ -0,0 +1,42 @@
/* eslint-disable camelcase */
import binding from './binding'
import { Lot } from './lot'
import { Chat } from './chat'
const debug = require('debug')('deltachat:node:chatlist')
interface NativeChatList {}
/**
* Wrapper around dc_chatlist_t*
*/
export class ChatList {
constructor(private dc_chatlist: NativeChatList) {
debug('ChatList constructor')
if (dc_chatlist === null) {
throw new Error('native chat list can not be null')
}
}
getChatId(index: number): number {
debug(`getChatId ${index}`)
return binding.dcn_chatlist_get_chat_id(this.dc_chatlist, index)
}
getCount(): number {
debug('getCount')
return binding.dcn_chatlist_get_cnt(this.dc_chatlist)
}
getMessageId(index: number): number {
debug(`getMessageId ${index}`)
return binding.dcn_chatlist_get_msg_id(this.dc_chatlist, index)
}
getSummary(index: number, chat?: Chat): Lot {
debug(`getSummary ${index}`)
const dc_chat = (chat && chat.dc_chat) || null
return new Lot(
binding.dcn_chatlist_get_summary(this.dc_chatlist, index, dc_chat)
)
}
}

260
node/lib/constants.ts Normal file
View File

@@ -0,0 +1,260 @@
// Generated!
export enum C {
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
DC_CERTCK_AUTO = 0,
DC_CERTCK_STRICT = 1,
DC_CHAT_ID_ALLDONE_HINT = 7,
DC_CHAT_ID_ARCHIVED_LINK = 6,
DC_CHAT_ID_LAST_SPECIAL = 9,
DC_CHAT_ID_TRASH = 3,
DC_CHAT_TYPE_BROADCAST = 160,
DC_CHAT_TYPE_GROUP = 120,
DC_CHAT_TYPE_MAILINGLIST = 140,
DC_CHAT_TYPE_SINGLE = 100,
DC_CHAT_TYPE_UNDEFINED = 0,
DC_CHAT_VISIBILITY_ARCHIVED = 1,
DC_CHAT_VISIBILITY_NORMAL = 0,
DC_CHAT_VISIBILITY_PINNED = 2,
DC_CONNECTIVITY_CONNECTED = 4000,
DC_CONNECTIVITY_CONNECTING = 2000,
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
DC_CONNECTIVITY_WORKING = 3000,
DC_CONTACT_ID_DEVICE = 5,
DC_CONTACT_ID_INFO = 2,
DC_CONTACT_ID_LAST_SPECIAL = 9,
DC_CONTACT_ID_SELF = 1,
DC_DOWNLOAD_AVAILABLE = 10,
DC_DOWNLOAD_DONE = 0,
DC_DOWNLOAD_FAILURE = 20,
DC_DOWNLOAD_IN_PROGRESS = 1000,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
DC_EVENT_CHAT_MODIFIED = 2020,
DC_EVENT_CONFIGURE_PROGRESS = 2041,
DC_EVENT_CONNECTIVITY_CHANGED = 2100,
DC_EVENT_CONTACTS_CHANGED = 2030,
DC_EVENT_DELETED_BLOB_FILE = 151,
DC_EVENT_ERROR = 400,
DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410,
DC_EVENT_IMAP_CONNECTED = 102,
DC_EVENT_IMAP_MESSAGE_DELETED = 104,
DC_EVENT_IMAP_MESSAGE_MOVED = 105,
DC_EVENT_IMEX_FILE_WRITTEN = 2052,
DC_EVENT_IMEX_PROGRESS = 2051,
DC_EVENT_INCOMING_MSG = 2005,
DC_EVENT_INFO = 100,
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
DC_EVENT_MSGS_NOTICED = 2008,
DC_EVENT_MSG_DELIVERED = 2010,
DC_EVENT_MSG_FAILED = 2012,
DC_EVENT_MSG_READ = 2015,
DC_EVENT_NEW_BLOB_FILE = 150,
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
DC_EVENT_SELFAVATAR_CHANGED = 2110,
DC_EVENT_SMTP_CONNECTED = 101,
DC_EVENT_SMTP_MESSAGE_SENT = 103,
DC_EVENT_WARNING = 300,
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
DC_GCL_ARCHIVED_ONLY = 1,
DC_GCL_FOR_FORWARDING = 8,
DC_GCL_NO_SPECIALS = 2,
DC_GCL_VERIFIED_ONLY = 1,
DC_GCM_ADDDAYMARKER = 1,
DC_GCM_INFO_ONLY = 2,
DC_IMEX_EXPORT_BACKUP = 11,
DC_IMEX_EXPORT_SELF_KEYS = 1,
DC_IMEX_IMPORT_BACKUP = 12,
DC_IMEX_IMPORT_SELF_KEYS = 2,
DC_INFO_PROTECTION_DISABLED = 12,
DC_INFO_PROTECTION_ENABLED = 11,
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,
DC_MEDIA_QUALITY_WORSE = 1,
DC_MSG_AUDIO = 40,
DC_MSG_FILE = 60,
DC_MSG_GIF = 21,
DC_MSG_ID_DAYMARKER = 9,
DC_MSG_ID_LAST_SPECIAL = 9,
DC_MSG_ID_MARKER1 = 1,
DC_MSG_IMAGE = 20,
DC_MSG_STICKER = 23,
DC_MSG_TEXT = 10,
DC_MSG_VIDEO = 50,
DC_MSG_VIDEOCHAT_INVITATION = 70,
DC_MSG_VOICE = 41,
DC_MSG_WEBXDC = 80,
DC_PROVIDER_STATUS_BROKEN = 3,
DC_PROVIDER_STATUS_OK = 1,
DC_PROVIDER_STATUS_PREPARATION = 2,
DC_QR_ACCOUNT = 250,
DC_QR_ADDR = 320,
DC_QR_ASK_VERIFYCONTACT = 200,
DC_QR_ASK_VERIFYGROUP = 202,
DC_QR_ERROR = 400,
DC_QR_FPR_MISMATCH = 220,
DC_QR_FPR_OK = 210,
DC_QR_FPR_WITHOUT_ADDR = 230,
DC_QR_REVIVE_VERIFYCONTACT = 510,
DC_QR_REVIVE_VERIFYGROUP = 512,
DC_QR_TEXT = 330,
DC_QR_URL = 332,
DC_QR_WEBRTC_INSTANCE = 260,
DC_QR_WITHDRAW_VERIFYCONTACT = 500,
DC_QR_WITHDRAW_VERIFYGROUP = 502,
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
DC_SHOW_EMAILS_ALL = 2,
DC_SHOW_EMAILS_OFF = 0,
DC_SOCKET_AUTO = 0,
DC_SOCKET_PLAIN = 3,
DC_SOCKET_SSL = 1,
DC_SOCKET_STARTTLS = 2,
DC_STATE_IN_FRESH = 10,
DC_STATE_IN_NOTICED = 13,
DC_STATE_IN_SEEN = 16,
DC_STATE_OUT_DELIVERED = 26,
DC_STATE_OUT_DRAFT = 19,
DC_STATE_OUT_FAILED = 24,
DC_STATE_OUT_MDN_RCVD = 28,
DC_STATE_OUT_PENDING = 20,
DC_STATE_OUT_PREPARING = 18,
DC_STATE_UNDEFINED = 0,
DC_STR_AC_SETUP_MSG_BODY = 43,
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BAD_TIME_MSG_BODY = 85,
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
DC_STR_CONTACT_NOT_VERIFIED = 36,
DC_STR_CONTACT_SETUP_CHANGED = 37,
DC_STR_CONTACT_VERIFIED = 35,
DC_STR_DEVICE_MESSAGES = 68,
DC_STR_DEVICE_MESSAGES_HINT = 70,
DC_STR_DOWNLOAD_AVAILABILITY = 100,
DC_STR_DRAFT = 3,
DC_STR_E2E_AVAILABLE = 25,
DC_STR_E2E_PREFERRED = 34,
DC_STR_ENCRYPTEDMSG = 24,
DC_STR_ENCR_NONE = 28,
DC_STR_ENCR_TRANSP = 27,
DC_STR_EPHEMERAL_DAY = 79,
DC_STR_EPHEMERAL_DAYS = 95,
DC_STR_EPHEMERAL_DISABLED = 75,
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
DC_STR_EPHEMERAL_HOUR = 78,
DC_STR_EPHEMERAL_HOURS = 94,
DC_STR_EPHEMERAL_MINUTE = 77,
DC_STR_EPHEMERAL_MINUTES = 93,
DC_STR_EPHEMERAL_SECONDS = 76,
DC_STR_EPHEMERAL_WEEK = 80,
DC_STR_EPHEMERAL_WEEKS = 96,
DC_STR_ERROR = 112,
DC_STR_ERROR_NO_NETWORK = 87,
DC_STR_FAILED_SENDING_TO = 74,
DC_STR_FILE = 12,
DC_STR_FINGERPRINTS = 30,
DC_STR_FORWARDED = 97,
DC_STR_GIF = 23,
DC_STR_IMAGE = 9,
DC_STR_INCOMING_MESSAGES = 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
DC_STR_LOCATION = 66,
DC_STR_MESSAGES = 114,
DC_STR_MSGACTIONBYME = 63,
DC_STR_MSGACTIONBYUSER = 62,
DC_STR_MSGADDMEMBER = 17,
DC_STR_MSGDELMEMBER = 18,
DC_STR_MSGGROUPLEFT = 19,
DC_STR_MSGGRPIMGCHANGED = 16,
DC_STR_MSGGRPIMGDELETED = 33,
DC_STR_MSGGRPNAME = 15,
DC_STR_MSGLOCATIONDISABLED = 65,
DC_STR_MSGLOCATIONENABLED = 64,
DC_STR_NOMESSAGES = 1,
DC_STR_NOT_CONNECTED = 121,
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
DC_STR_ONE_MOMENT = 106,
DC_STR_OUTGOING_MESSAGES = 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
DC_STR_SECURE_JOIN_REPLIES = 118,
DC_STR_SECURE_JOIN_STARTED = 117,
DC_STR_SELF = 2,
DC_STR_SELF_DELETED_MSG_BODY = 91,
DC_STR_SENDING = 110,
DC_STR_SERVER_TURNED_OFF = 92,
DC_STR_SETUP_CONTACT_QR_DESC = 119,
DC_STR_STICKER = 67,
DC_STR_STORAGE_ON_DOMAIN = 105,
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
DC_STR_SYNC_MSG_BODY = 102,
DC_STR_SYNC_MSG_SUBJECT = 101,
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
DC_STR_UPDATING = 109,
DC_STR_VIDEO = 10,
DC_STR_VIDEOCHAT_INVITATION = 82,
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
DC_STR_VOICEMESSAGE = 7,
DC_STR_WELCOME_MESSAGE = 71,
DC_TEXT1_DRAFT = 1,
DC_TEXT1_SELF = 3,
DC_TEXT1_USERNAME = 2,
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
DC_VIDEOCHATTYPE_JITSI = 2,
DC_VIDEOCHATTYPE_UNKNOWN = 0,
}
// Generated!
export const EventId2EventName: { [key: number]: string } = {
100: 'DC_EVENT_INFO',
101: 'DC_EVENT_SMTP_CONNECTED',
102: 'DC_EVENT_IMAP_CONNECTED',
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
150: 'DC_EVENT_NEW_BLOB_FILE',
151: 'DC_EVENT_DELETED_BLOB_FILE',
300: 'DC_EVENT_WARNING',
400: 'DC_EVENT_ERROR',
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',
2015: 'DC_EVENT_MSG_READ',
2020: 'DC_EVENT_CHAT_MODIFIED',
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
2030: 'DC_EVENT_CONTACTS_CHANGED',
2035: 'DC_EVENT_LOCATION_CHANGED',
2041: 'DC_EVENT_CONFIGURE_PROGRESS',
2051: 'DC_EVENT_IMEX_PROGRESS',
2052: 'DC_EVENT_IMEX_FILE_WRITTEN',
2060: 'DC_EVENT_SECUREJOIN_INVITER_PROGRESS',
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
}

94
node/lib/contact.ts Normal file
View File

@@ -0,0 +1,94 @@
import { integerToHexColor } from './util'
/* eslint-disable camelcase */
import binding from './binding'
const debug = require('debug')('deltachat:node:contact')
interface NativeContact {}
/**
* Wrapper around dc_contact_t*
*/
export class Contact {
constructor(public dc_contact: NativeContact) {
debug('Contact constructor')
if (dc_contact === null) {
throw new Error('native contact can not be null')
}
}
toJson() {
debug('toJson')
return {
address: this.getAddress(),
color: this.color,
authName: this.authName,
status: this.status,
displayName: this.getDisplayName(),
id: this.getId(),
lastSeen: this.lastSeen,
name: this.getName(),
profileImage: this.getProfileImage(),
nameAndAddr: this.getNameAndAddress(),
isBlocked: this.isBlocked(),
isVerified: this.isVerified(),
}
}
getAddress(): string {
return binding.dcn_contact_get_addr(this.dc_contact)
}
/** Get original contact name.
* This is the name of the contact as defined by the contact themself.
* If the contact themself does not define such a name,
* an empty string is returned. */
get authName(): string {
return binding.dcn_contact_get_auth_name(this.dc_contact)
}
get color(): string {
return integerToHexColor(binding.dcn_contact_get_color(this.dc_contact))
}
/**
* contact's status
*
* Status is the last signature received in a message from this contact.
*/
get status(): string {
return binding.dcn_contact_get_status(this.dc_contact)
}
getDisplayName(): string {
return binding.dcn_contact_get_display_name(this.dc_contact)
}
getId(): number {
return binding.dcn_contact_get_id(this.dc_contact)
}
get lastSeen(): number {
return binding.dcn_contact_get_last_seen(this.dc_contact)
}
getName(): string {
return binding.dcn_contact_get_name(this.dc_contact)
}
getNameAndAddress(): string {
return binding.dcn_contact_get_name_n_addr(this.dc_contact)
}
getProfileImage(): string {
return binding.dcn_contact_get_profile_image(this.dc_contact)
}
isBlocked() {
return Boolean(binding.dcn_contact_is_blocked(this.dc_contact))
}
isVerified() {
return Boolean(binding.dcn_contact_is_verified(this.dc_contact))
}
}

955
node/lib/context.ts Normal file
View File

@@ -0,0 +1,955 @@
/* eslint-disable camelcase */
import binding from './binding'
import { C, EventId2EventName } from './constants'
import { Chat } from './chat'
import { ChatList } from './chatlist'
import { Contact } from './contact'
import { Message } from './message'
import { Lot } from './lot'
import { Locations } from './locations'
import rawDebug from 'debug'
import { AccountManager } from './deltachat'
import { join } from 'path'
import { EventEmitter } from 'stream'
const debug = rawDebug('deltachat:node:index')
const noop = function () {}
interface NativeContext {}
/**
* Wrapper around dcn_context_t*
*
* only acts as event emitter when created in standalone mode (without account manager)
* with `Context.open`
*/
export class Context extends EventEmitter {
constructor(
readonly manager: AccountManager | null,
private inner_dcn_context: NativeContext,
readonly account_id: number | null
) {
super()
debug('DeltaChat constructor')
if (inner_dcn_context === null) {
throw new Error('inner_dcn_context can not be null')
}
}
/** Opens a stanalone context (without an account manager)
* automatically starts the event handler */
static open(cwd: string): Context {
const dbFile = join(cwd, 'db.sqlite')
const context = new Context(null, binding.dcn_context_new(dbFile), null)
debug('Opened context')
function handleCoreEvent(
eventId: number,
data1: number,
data2: number | string
) {
const eventString = EventId2EventName[eventId]
debug(eventString, data1, data2)
if (!context.emit) {
console.log('Received an event but EventEmitter is already destroyed.')
console.log(eventString, data1, data2)
return
}
context.emit(eventString, data1, data2)
context.emit('ALL', eventString, data1, data2)
}
binding.dcn_start_event_handler(
context.dcn_context,
handleCoreEvent.bind(this)
)
debug('Started event handler')
return context
}
get dcn_context() {
return this.inner_dcn_context
}
get is_open() {
return Boolean(binding.dcn_context_is_open())
}
open(passphrase?: string) {
return Boolean(
binding.dcn_context_open(this.dcn_context, passphrase ? passphrase : '')
)
}
unref() {
binding.dcn_context_unref(this.dcn_context)
;(this.inner_dcn_context as any) = null
}
acceptChat(chatId: number) {
binding.dcn_accept_chat(this.dcn_context, chatId)
}
blockChat(chatId: number) {
binding.dcn_block_chat(this.dcn_context, chatId)
}
addAddressBook(addressBook: string) {
debug(`addAddressBook ${addressBook}`)
return binding.dcn_add_address_book(this.dcn_context, addressBook)
}
addContactToChat(chatId: number, contactId: number) {
debug(`addContactToChat ${chatId} ${contactId}`)
return Boolean(
binding.dcn_add_contact_to_chat(
this.dcn_context,
Number(chatId),
Number(contactId)
)
)
}
addDeviceMessage(label: string, msg: Message | string) {
debug(`addDeviceMessage ${label} ${msg}`)
if (!msg) {
throw new Error('invalid msg parameter')
}
if (typeof label !== 'string') {
throw new Error('invalid label parameter, must be a string')
}
if (typeof msg === 'string') {
const msgObj = this.messageNew()
msgObj.setText(msg)
msg = msgObj
}
if (!msg.dc_msg) {
throw new Error('invalid msg object')
}
return binding.dcn_add_device_msg(this.dcn_context, label, msg.dc_msg)
}
setChatVisibility(
chatId: number,
visibility:
| C.DC_CHAT_VISIBILITY_NORMAL
| C.DC_CHAT_VISIBILITY_ARCHIVED
| C.DC_CHAT_VISIBILITY_PINNED
) {
debug(`setChatVisibility ${chatId} ${visibility}`)
binding.dcn_set_chat_visibility(
this.dcn_context,
Number(chatId),
visibility
)
}
blockContact(contactId: number, block: boolean) {
debug(`blockContact ${contactId} ${block}`)
binding.dcn_block_contact(
this.dcn_context,
Number(contactId),
block ? 1 : 0
)
}
checkQrCode(qrCode: string) {
debug(`checkQrCode ${qrCode}`)
const dc_lot = binding.dcn_check_qr(this.dcn_context, qrCode)
let result = dc_lot ? new Lot(dc_lot) : null
if (result) {
return { id: result.getId(), ...result.toJson() }
}
return result
}
configure(opts: any): Promise<void> {
return new Promise((resolve, reject) => {
debug('configure')
const onSuccess = () => {
removeListeners()
resolve()
}
const onFail = (error: string) => {
removeListeners()
reject(new Error(error))
}
let onConfigure: (...args: any[]) => void
if (this.account_id === null) {
onConfigure = (data1: number, data2: string) => {
if (data1 === 0) return onFail(data2)
else if (data1 === 1000) return onSuccess()
}
} else {
onConfigure = (accountId: number, data1: number, data2: string) => {
if (this.account_id !== accountId) {
return
}
if (data1 === 0) return onFail(data2)
else if (data1 === 1000) return onSuccess()
}
}
const removeListeners = () => {
;(this.manager || this).removeListener(
'DC_EVENT_CONFIGURE_PROGRESS',
onConfigure
)
}
const registerListeners = () => {
;(this.manager || this).on('DC_EVENT_CONFIGURE_PROGRESS', onConfigure)
}
registerListeners()
if (!opts) opts = {}
Object.keys(opts).forEach((key) => {
const value = opts[key]
this.setConfig(key, value)
})
binding.dcn_configure(this.dcn_context)
})
}
continueKeyTransfer(messageId: number, setupCode: string) {
debug(`continueKeyTransfer ${messageId}`)
return new Promise((resolve, reject) => {
binding.dcn_continue_key_transfer(
this.dcn_context,
Number(messageId),
setupCode,
(result: number) => resolve(result === 1)
)
})
}
/** @returns chatId */
createBroadcastList(): number {
debug(`createBroadcastList`)
return binding.dcn_create_broadcast_list(this.dcn_context)
}
/** @returns chatId */
createChatByContactId(contactId: number): number {
debug(`createChatByContactId ${contactId}`)
return binding.dcn_create_chat_by_contact_id(
this.dcn_context,
Number(contactId)
)
}
/** @returns contactId */
createContact(name: string, addr: string): number {
debug(`createContact ${name} ${addr}`)
return binding.dcn_create_contact(this.dcn_context, name, addr)
}
/**
*
* @param chatName The name of the chat that should be created
* @param is_protected Whether the chat should be protected at creation time
* @returns chatId
*/
createGroupChat(chatName: string, is_protected: boolean = false): number {
debug(`createGroupChat ${chatName} [protected:${is_protected}]`)
return binding.dcn_create_group_chat(
this.dcn_context,
is_protected ? 1 : 0,
chatName
)
}
deleteChat(chatId: number) {
debug(`deleteChat ${chatId}`)
binding.dcn_delete_chat(this.dcn_context, Number(chatId))
}
deleteContact(contactId: number) {
debug(`deleteContact ${contactId}`)
return Boolean(
binding.dcn_delete_contact(this.dcn_context, Number(contactId))
)
}
deleteMessages(messageIds: number[]) {
if (!Array.isArray(messageIds)) {
messageIds = [messageIds]
}
messageIds = messageIds.map((id) => Number(id))
debug('deleteMessages', messageIds)
binding.dcn_delete_msgs(this.dcn_context, messageIds)
}
forwardMessages(messageIds: number[], chatId: number) {
if (!Array.isArray(messageIds)) {
messageIds = [messageIds]
}
messageIds = messageIds.map((id) => Number(id))
debug('forwardMessages', messageIds)
binding.dcn_forward_msgs(this.dcn_context, messageIds, chatId)
}
getBlobdir(): string {
debug('getBlobdir')
return binding.dcn_get_blobdir(this.dcn_context)
}
getBlockedCount(): number {
debug('getBlockedCount')
return binding.dcn_get_blocked_cnt(this.dcn_context)
}
getBlockedContacts(): number[] {
debug('getBlockedContacts')
return binding.dcn_get_blocked_contacts(this.dcn_context)
}
getChat(chatId: number) {
debug(`getChat ${chatId}`)
const dc_chat = binding.dcn_get_chat(this.dcn_context, Number(chatId))
return dc_chat ? new Chat(dc_chat) : null
}
getChatContacts(chatId: number): number[] {
debug(`getChatContacts ${chatId}`)
return binding.dcn_get_chat_contacts(this.dcn_context, Number(chatId))
}
getChatIdByContactId(contactId: number): number {
debug(`getChatIdByContactId ${contactId}`)
return binding.dcn_get_chat_id_by_contact_id(
this.dcn_context,
Number(contactId)
)
}
getChatMedia(
chatId: number,
msgType1: number,
msgType2: number,
msgType3: number
): number[] {
debug(`getChatMedia ${chatId}`)
return binding.dcn_get_chat_media(
this.dcn_context,
Number(chatId),
msgType1,
msgType2 || 0,
msgType3 || 0
)
}
getMimeHeaders(messageId: number): string {
debug(`getMimeHeaders ${messageId}`)
return binding.dcn_get_mime_headers(this.dcn_context, Number(messageId))
}
getChatlistItemSummary(chatId: number, messageId: number) {
debug(`getChatlistItemSummary ${chatId} ${messageId}`)
return new Lot(
binding.dcn_chatlist_get_summary2(this.dcn_context, chatId, messageId)
)
}
getChatMessages(chatId: number, flags: number, marker1before: number) {
debug(`getChatMessages ${chatId} ${flags} ${marker1before}`)
return binding.dcn_get_chat_msgs(
this.dcn_context,
Number(chatId),
flags,
marker1before
)
}
/**
* Get encryption info for a chat.
* Get a multi-line encryption info, containing encryption preferences of all members.
* Can be used to find out why messages sent to group are not encrypted.
*
* @param chatId ID of the chat to get the encryption info for.
* @return Multi-line text, must be released using dc_str_unref() after usage.
*/
getChatEncrytionInfo(chatId: number): string {
return binding.dcn_get_chat_encrinfo(this.dcn_context, chatId)
}
getChats(listFlags: number, queryStr: string, queryContactId: number) {
debug('getChats')
const result = []
const list = this.getChatList(listFlags, queryStr, queryContactId)
const count = list.getCount()
for (let i = 0; i < count; i++) {
result.push(list.getChatId(i))
}
return result
}
getChatList(listFlags: number, queryStr: string, queryContactId: number) {
listFlags = listFlags || 0
queryStr = queryStr || ''
queryContactId = queryContactId || 0
debug(`getChatList ${listFlags} ${queryStr} ${queryContactId}`)
return new ChatList(
binding.dcn_get_chatlist(
this.dcn_context,
listFlags,
queryStr,
Number(queryContactId)
)
)
}
getConfig(key: string): string {
debug(`getConfig ${key}`)
return binding.dcn_get_config(this.dcn_context, key)
}
getContact(contactId: number) {
debug(`getContact ${contactId}`)
const dc_contact = binding.dcn_get_contact(
this.dcn_context,
Number(contactId)
)
return dc_contact ? new Contact(dc_contact) : null
}
getContactEncryptionInfo(contactId: number) {
debug(`getContactEncryptionInfo ${contactId}`)
return binding.dcn_get_contact_encrinfo(this.dcn_context, Number(contactId))
}
getContacts(listFlags: number, query: string) {
listFlags = listFlags || 0
query = query || ''
debug(`getContacts ${listFlags} ${query}`)
return binding.dcn_get_contacts(this.dcn_context, listFlags, query)
}
wasDeviceMessageEverAdded(label: string) {
debug(`wasDeviceMessageEverAdded ${label}`)
const added = binding.dcn_was_device_msg_ever_added(this.dcn_context, label)
return added === 1
}
getDraft(chatId: number) {
debug(`getDraft ${chatId}`)
const dc_msg = binding.dcn_get_draft(this.dcn_context, Number(chatId))
return dc_msg ? new Message(dc_msg) : null
}
getFreshMessageCount(chatId: number): number {
debug(`getFreshMessageCount ${chatId}`)
return binding.dcn_get_fresh_msg_cnt(this.dcn_context, Number(chatId))
}
getFreshMessages() {
debug('getFreshMessages')
return binding.dcn_get_fresh_msgs(this.dcn_context)
}
getInfo() {
debug('getInfo')
const info = binding.dcn_get_info(this.dcn_context)
return AccountManager.parseGetInfo(info)
}
getMessage(messageId: number) {
debug(`getMessage ${messageId}`)
const dc_msg = binding.dcn_get_msg(this.dcn_context, Number(messageId))
return dc_msg ? new Message(dc_msg) : null
}
getMessageCount(chatId: number): number {
debug(`getMessageCount ${chatId}`)
return binding.dcn_get_msg_cnt(this.dcn_context, Number(chatId))
}
getMessageInfo(messageId: number): string {
debug(`getMessageInfo ${messageId}`)
return binding.dcn_get_msg_info(this.dcn_context, Number(messageId))
}
getMessageHTML(messageId: number): string {
debug(`getMessageHTML ${messageId}`)
return binding.dcn_get_msg_html(this.dcn_context, Number(messageId))
}
getNextMediaMessage(
messageId: number,
msgType1: number,
msgType2: number,
msgType3: number
) {
debug(
`getNextMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
)
return this._getNextMedia(messageId, 1, msgType1, msgType2, msgType3)
}
getPreviousMediaMessage(
messageId: number,
msgType1: number,
msgType2: number,
msgType3: number
) {
debug(
`getPreviousMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
)
return this._getNextMedia(messageId, -1, msgType1, msgType2, msgType3)
}
_getNextMedia(
messageId: number,
dir: number,
msgType1: number,
msgType2: number,
msgType3: number
): number {
return binding.dcn_get_next_media(
this.dcn_context,
Number(messageId),
dir,
msgType1 || 0,
msgType2 || 0,
msgType3 || 0
)
}
getSecurejoinQrCode(chatId: number): string {
debug(`getSecurejoinQrCode ${chatId}`)
return binding.dcn_get_securejoin_qr(this.dcn_context, Number(chatId))
}
getSecurejoinQrCodeSVG(chatId: number): string {
debug(`getSecurejoinQrCodeSVG ${chatId}`)
return binding.dcn_get_securejoin_qr_svg(this.dcn_context, chatId)
}
startIO(): void {
debug(`startIO`)
binding.dcn_start_io(this.dcn_context)
}
stopIO(): void {
debug(`stopIO`)
binding.dcn_stop_io(this.dcn_context)
}
stopOngoingProcess(): void {
debug(`stopOngoingProcess`)
binding.dcn_stop_ongoing_process(this.dcn_context)
}
/**
*
* @deprectated please use `AccountManager.getSystemInfo()` instead
*/
static getSystemInfo() {
return AccountManager.getSystemInfo()
}
getConnectivity(): number {
return binding.dcn_get_connectivity(this.dcn_context)
}
getConnectivityHTML(): String {
return binding.dcn_get_connectivity_html(this.dcn_context)
}
importExport(what: number, param1: string, param2 = '') {
debug(`importExport ${what} ${param1} ${param2}`)
binding.dcn_imex(this.dcn_context, what, param1, param2)
}
importExportHasBackup(dir: string) {
debug(`importExportHasBackup ${dir}`)
return binding.dcn_imex_has_backup(this.dcn_context, dir)
}
initiateKeyTransfer(): Promise<string> {
return new Promise((resolve, reject) => {
debug('initiateKeyTransfer2')
binding.dcn_initiate_key_transfer(this.dcn_context, resolve)
})
}
isConfigured() {
debug('isConfigured')
return Boolean(binding.dcn_is_configured(this.dcn_context))
}
isContactInChat(chatId: number, contactId: number) {
debug(`isContactInChat ${chatId} ${contactId}`)
return Boolean(
binding.dcn_is_contact_in_chat(
this.dcn_context,
Number(chatId),
Number(contactId)
)
)
}
/**
*
* @returns resulting chat id or 0 on error
*/
joinSecurejoin(qrCode: string): number {
debug(`joinSecurejoin ${qrCode}`)
return binding.dcn_join_securejoin(this.dcn_context, qrCode)
}
lookupContactIdByAddr(addr: string): number {
debug(`lookupContactIdByAddr ${addr}`)
return binding.dcn_lookup_contact_id_by_addr(this.dcn_context, addr)
}
markNoticedChat(chatId: number) {
debug(`markNoticedChat ${chatId}`)
binding.dcn_marknoticed_chat(this.dcn_context, Number(chatId))
}
markSeenMessages(messageIds: number[]) {
if (!Array.isArray(messageIds)) {
messageIds = [messageIds]
}
messageIds = messageIds.map((id) => Number(id))
debug('markSeenMessages', messageIds)
binding.dcn_markseen_msgs(this.dcn_context, messageIds)
}
maybeNetwork() {
debug('maybeNetwork')
binding.dcn_maybe_network(this.dcn_context)
}
messageNew(viewType = C.DC_MSG_TEXT) {
debug(`messageNew ${viewType}`)
return new Message(binding.dcn_msg_new(this.dcn_context, viewType))
}
removeContactFromChat(chatId: number, contactId: number) {
debug(`removeContactFromChat ${chatId} ${contactId}`)
return Boolean(
binding.dcn_remove_contact_from_chat(
this.dcn_context,
Number(chatId),
Number(contactId)
)
)
}
/**
*
* @param chatId ID of the chat to search messages in. Set this to 0 for a global search.
* @param query The query to search for.
*/
searchMessages(chatId: number, query: string): number[] {
debug(`searchMessages ${chatId} ${query}`)
return binding.dcn_search_msgs(this.dcn_context, Number(chatId), query)
}
sendMessage(chatId: number, msg: string | Message) {
debug(`sendMessage ${chatId}`)
if (!msg) {
throw new Error('invalid msg parameter')
}
if (typeof msg === 'string') {
const msgObj = this.messageNew()
msgObj.setText(msg)
msg = msgObj
}
if (!msg.dc_msg) {
throw new Error('invalid msg object')
}
return binding.dcn_send_msg(this.dcn_context, Number(chatId), msg.dc_msg)
}
downloadFullMessage(messageId: number) {
binding.dcn_download_full_msg(this.dcn_context, messageId)
}
/**
*
* @returns {Promise<number>} Promise that resolves into the resulting message id
*/
sendVideochatInvitation(chatId: number): Promise<number> {
debug(`sendVideochatInvitation ${chatId}`)
return new Promise((resolve, reject) => {
binding.dcn_send_videochat_invitation(
this.dcn_context,
chatId,
(result: number) => {
if (result !== 0) {
resolve(result)
} else {
reject(
'Videochatinvitation failed to send, see error events for detailed info'
)
}
}
)
})
}
setChatName(chatId: number, name: string) {
debug(`setChatName ${chatId} ${name}`)
return Boolean(
binding.dcn_set_chat_name(this.dcn_context, Number(chatId), name)
)
}
/**
*
* @param chatId
* @param protect
* @returns success boolean
*/
setChatProtection(chatId: number, protect: boolean) {
debug(`setChatProtection ${chatId} ${protect}`)
return Boolean(
binding.dcn_set_chat_protection(
this.dcn_context,
Number(chatId),
protect ? 1 : 0
)
)
}
getChatEphemeralTimer(chatId: number): number {
debug(`getChatEphemeralTimer ${chatId}`)
return binding.dcn_get_chat_ephemeral_timer(
this.dcn_context,
Number(chatId)
)
}
setChatEphemeralTimer(chatId: number, timer: number) {
debug(`setChatEphemeralTimer ${chatId} ${timer}`)
return Boolean(
binding.dcn_set_chat_ephemeral_timer(
this.dcn_context,
Number(chatId),
Number(timer)
)
)
}
setChatProfileImage(chatId: number, image: string) {
debug(`setChatProfileImage ${chatId} ${image}`)
return Boolean(
binding.dcn_set_chat_profile_image(
this.dcn_context,
Number(chatId),
image || ''
)
)
}
setConfig(key: string, value: string | boolean | number): number {
debug(`setConfig (string) ${key} ${value}`)
if (value === null) {
return binding.dcn_set_config_null(this.dcn_context, key)
} else {
if (typeof value === 'boolean') {
value = value === true ? '1' : '0'
} else if (typeof value === 'number') {
value = String(value)
}
return binding.dcn_set_config(this.dcn_context, key, value)
}
}
setConfigFromQr(qrcodeContent: string): boolean {
return Boolean(
binding.dcn_set_config_from_qr(this.dcn_context, qrcodeContent)
)
}
estimateDeletionCount(fromServer: boolean, seconds: number): number {
debug(`estimateDeletionCount fromServer: ${fromServer} seconds: ${seconds}`)
return binding.dcn_estimate_deletion_cnt(
this.dcn_context,
fromServer === true ? 1 : 0,
seconds
)
}
setStockTranslation(stockId: number, stockMsg: string) {
debug(`setStockTranslation ${stockId} ${stockMsg}`)
return Boolean(
binding.dcn_set_stock_translation(
this.dcn_context,
Number(stockId),
stockMsg
)
)
}
setDraft(chatId: number, msg: Message | null) {
debug(`setDraft ${chatId}`)
binding.dcn_set_draft(
this.dcn_context,
Number(chatId),
msg ? msg.dc_msg : null
)
}
setLocation(latitude: number, longitude: number, accuracy: number) {
debug(`setLocation ${latitude}`)
binding.dcn_set_location(
this.dcn_context,
Number(latitude),
Number(longitude),
Number(accuracy)
)
}
/*
* @param chatId Chat-id to get location information for.
* 0 to get locations independently of the chat.
* @param contactId Contact id to get location information for.
* If also a chat-id is given, this should be a member of the given chat.
* 0 to get locations independently of the contact.
* @param timestampFrom Start of timespan to return.
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
* 0 for "start from the beginning".
* @param timestampTo End of timespan to return.
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
* 0 for "all up to now".
* @return Array of locations, NULL is never returned.
* The array is sorted decending;
* the first entry in the array is the location with the newest timestamp.
*
* Examples:
* // get locations from the last hour for a global map
* getLocations(0, 0, time(NULL)-60*60, 0);
*
* // get locations from a contact for a global map
* getLocations(0, contact_id, 0, 0);
*
* // get all locations known for a given chat
* getLocations(chat_id, 0, 0, 0);
*
* // get locations from a single contact for a given chat
* getLocations(chat_id, contact_id, 0, 0);
*/
getLocations(
chatId: number,
contactId: number,
timestampFrom = 0,
timestampTo = 0
) {
const locations = new Locations(
binding.dcn_get_locations(
this.dcn_context,
Number(chatId),
Number(contactId),
timestampFrom,
timestampTo
)
)
return locations.toJson()
}
/**
*
* @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds)
*/
setChatMuteDuration(chatId: number, duration: number) {
return Boolean(
binding.dcn_set_chat_mute_duration(this.dcn_context, chatId, duration)
)
}
/** get information about the provider */
getProviderFromEmail(email: string) {
debug('DeltaChat.getProviderFromEmail')
const provider = binding.dcn_provider_new_from_email(
this.dcn_context,
email
)
if (!provider) {
return undefined
}
return {
before_login_hint: binding.dcn_provider_get_before_login_hint(provider),
overview_page: binding.dcn_provider_get_overview_page(provider),
status: binding.dcn_provider_get_status(provider),
}
}
sendWebxdcStatusUpdate<T>(
msgId: number,
json: WebxdcSendingStatusUpdate<T>,
descr: string
) {
return Boolean(
binding.dcn_send_webxdc_status_update(
this.dcn_context,
msgId,
JSON.stringify(json),
descr
)
)
}
getWebxdcStatusUpdates<T>(
msgId: number,
serial = 0
): WebxdcReceivedStatusUpdate<T>[] {
return JSON.parse(
binding.dcn_get_webxdc_status_updates(this.dcn_context, msgId, serial)
)
}
/** the string contains the binary data, it is an "u8 string", maybe we will use a more efficient type in the future. */
getWebxdcBlob(message: Message, filename: string): Buffer | null {
return binding.dcn_msg_get_webxdc_blob(message.dc_msg, filename)
}
}
export type WebxdcInfo = {
name: string
icon: string
summary: string
/**
* if set by the webxdc, name of the document in edit
*/
document?: string
}
type WebxdcSendingStatusUpdate<T> = {
/** the payload, deserialized json:
* any javascript primitive, array or object. */
payload: T
/** optional, short, informational message that will be added to the chat,
* eg. "Alice voted" or "Bob scored 123 in MyGame";
* usually only one line of text is shown,
* use this option sparingly to not spam the chat. */
info?: string
/** optional, short text, shown beside app icon;
* it is recommended to use some aggregated value,
* eg. "8 votes", "Highscore: 123" */
summary?: string
/**
* optional, name of the document in edit,
* must not be used eg. in games where the Webxdc does not create documents
*/
document?: string
}
type WebxdcReceivedStatusUpdate<T> = {
/** the payload, deserialized json */
payload: T
/** the serial number of this update. Serials are larger `0` and newer serials have higher numbers. */
serial: number
/** the maximum serial currently known.
* If `max_serial` equals `serial` this update is the last update (until new network messages arrive). */
max_serial: number
/** optional, short, informational message. */
info?: string
/** optional, short text, shown beside app icon. If there are no updates, an empty JSON-array is returned. */
summary?: string
}

205
node/lib/deltachat.ts Normal file
View File

@@ -0,0 +1,205 @@
/* eslint-disable camelcase */
import binding from './binding'
import { EventId2EventName } from './constants'
import { EventEmitter } from 'events'
import { existsSync } from 'fs'
import rawDebug from 'debug'
import { tmpdir } from 'os'
import { join } from 'path'
import { Context } from './context'
const debug = rawDebug('deltachat:node:index')
const noop = function () {}
interface NativeAccount {}
/**
* Wrapper around dcn_account_t*
*/
export class AccountManager extends EventEmitter {
dcn_accounts: NativeAccount
accountDir: string
constructor(cwd: string, os = 'deltachat-node') {
debug('DeltaChat constructor')
super()
this.accountDir = cwd
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
}
getAllAccountIds() {
return binding.dcn_accounts_get_all(this.dcn_accounts)
}
selectAccount(account_id: number) {
return binding.dcn_accounts_select_account(this.dcn_accounts, account_id)
}
selectedAccount(): number {
return binding.dcn_accounts_get_selected_account(this.dcn_accounts)
}
addAccount(): number {
return binding.dcn_accounts_add_account(this.dcn_accounts)
}
addClosedAccount(): number {
return binding.dcn_accounts_add_closed_account(this.dcn_accounts)
}
removeAccount(account_id: number) {
return binding.dcn_accounts_remove_account(this.dcn_accounts, account_id)
}
accountContext(account_id: number) {
const native_context = binding.dcn_accounts_get_account(
this.dcn_accounts,
account_id
)
if (native_context === null) {
throw new Error(
`could not get context with id ${account_id}, does it even exist? please check your ids`
)
}
return new Context(this, native_context, account_id)
}
migrateAccount(dbfile: string): number {
return binding.dcn_accounts_migrate_account(this.dcn_accounts, dbfile)
}
close() {
this.stopIO()
debug('unrefing context')
binding.dcn_accounts_unref(this.dcn_accounts)
debug('Unref end')
}
emit(
event: string | symbol,
account_id: number,
data1: any,
data2: any
): boolean {
super.emit('ALL', event, account_id, data1, data2)
return super.emit(event, account_id, data1, data2)
}
handleCoreEvent(
eventId: number,
accountId: number,
data1: number,
data2: number | string
) {
const eventString = EventId2EventName[eventId]
debug('event', eventString, accountId, data1, data2)
debug(eventString, data1, data2)
if (!this.emit) {
console.log('Received an event but EventEmitter is already destroyed.')
console.log(eventString, data1, data2)
return
}
this.emit(eventString, accountId, data1, data2)
}
startEvents() {
if (this.dcn_accounts === null) {
throw new Error('dcn_account is null')
}
binding.dcn_accounts_start_event_handler(
this.dcn_accounts,
this.handleCoreEvent.bind(this)
)
debug('Started event handler')
}
startIO() {
binding.dcn_accounts_start_io(this.dcn_accounts)
}
stopIO() {
binding.dcn_accounts_stop_io(this.dcn_accounts)
}
static maybeValidAddr(addr: string) {
debug('DeltaChat.maybeValidAddr')
if (addr === null) return false
return Boolean(binding.dcn_maybe_valid_addr(addr))
}
static parseGetInfo(info: string) {
debug('static _getInfo')
const result: { [key: string]: string } = {}
const regex = /^(\w+)=(.*)$/i
info
.split('\n')
.filter(Boolean)
.forEach((line) => {
const match = regex.exec(line)
if (match) {
result[match[1]] = match[2]
}
})
return result
}
static newTemporary() {
let directory = null
while (true) {
const randomString = Math.random().toString(36).substr(2, 5)
directory = join(tmpdir(), 'deltachat-' + randomString)
if (!existsSync(directory)) break
}
const dc = new AccountManager(directory)
const accountId = dc.addAccount()
const context = dc.accountContext(accountId)
return { dc, context, accountId, directory }
}
static getSystemInfo() {
debug('DeltaChat.getSystemInfo')
const { dc, context } = AccountManager.newTemporary()
const info = AccountManager.parseGetInfo(
binding.dcn_get_info(context.dcn_context)
)
const {
deltachat_core_version,
sqlite_version,
sqlite_thread_safe,
libetpan_version,
openssl_version,
compile_date,
arch,
} = info
const result = {
deltachat_core_version,
sqlite_version,
sqlite_thread_safe,
libetpan_version,
openssl_version,
compile_date,
arch,
}
context.unref()
dc.close()
return result
}
/** get information about the provider
*
* This function creates a temporary context to be standalone,
* if posible use `Context.getProviderFromEmail` instead. (otherwise potential proxy settings are not used)
* @deprecated
*/
static getProviderFromEmail(email: string) {
debug('DeltaChat.getProviderFromEmail')
const { dc, context } = AccountManager.newTemporary()
const provider = context.getProviderFromEmail(email)
context.unref()
dc.close()
return provider
}
}

20
node/lib/index.ts Normal file
View File

@@ -0,0 +1,20 @@
import { AccountManager } from './deltachat'
export default AccountManager
export { Context } from './context'
export { Chat } from './chat'
export { ChatList } from './chatlist'
export { C } from './constants'
export { Contact } from './contact'
export { AccountManager as DeltaChat }
export { Locations } from './locations'
export { Lot } from './lot'
export {
Message,
MessageState,
MessageViewType,
MessageDownloadState,
} from './message'
export * from './types'

82
node/lib/locations.ts Normal file
View File

@@ -0,0 +1,82 @@
/* eslint-disable camelcase */
const binding = require('../binding')
const debug = require('debug')('deltachat:node:locations')
interface NativeLocations {}
/**
* Wrapper around dc_location_t*
*/
export class Locations {
constructor(public dc_locations: NativeLocations) {
debug('Locations constructor')
if (dc_locations === null) {
throw new Error('dc_locations can not be null')
}
}
locationToJson(index: number) {
debug('locationToJson')
return {
accuracy: this.getAccuracy(index),
latitude: this.getLatitude(index),
longitude: this.getLongitude(index),
timestamp: this.getTimestamp(index),
contactId: this.getContactId(index),
msgId: this.getMsgId(index),
chatId: this.getChatId(index),
isIndependent: this.isIndependent(index),
marker: this.getMarker(index),
}
}
toJson(): ReturnType<Locations['locationToJson']>[] {
debug('toJson')
const locations = []
const count = this.getCount()
for (let index = 0; index < count; index++) {
locations.push(this.locationToJson(index))
}
return locations
}
getCount(): number {
return binding.dcn_array_get_cnt(this.dc_locations)
}
getAccuracy(index: number): number {
return binding.dcn_array_get_accuracy(this.dc_locations, index)
}
getLatitude(index: number): number {
return binding.dcn_array_get_latitude(this.dc_locations, index)
}
getLongitude(index: number): number {
return binding.dcn_array_get_longitude(this.dc_locations, index)
}
getTimestamp(index: number): number {
return binding.dcn_array_get_timestamp(this.dc_locations, index)
}
getMsgId(index: number): number {
return binding.dcn_array_get_msg_id(this.dc_locations, index)
}
getContactId(index: number): number {
return binding.dcn_array_get_contact_id(this.dc_locations, index)
}
getChatId(index: number): number {
return binding.dcn_array_get_chat_id(this.dc_locations, index)
}
isIndependent(index: number): boolean {
return binding.dcn_array_is_independent(this.dc_locations, index)
}
getMarker(index: number): string {
return binding.dcn_array_get_marker(this.dc_locations, index)
}
}

52
node/lib/lot.ts Normal file
View File

@@ -0,0 +1,52 @@
/* eslint-disable camelcase */
const binding = require('../binding')
const debug = require('debug')('deltachat:node:lot')
interface NativeLot {}
/**
* Wrapper around dc_lot_t*
*/
export class Lot {
constructor(public dc_lot: NativeLot) {
debug('Lot constructor')
if (dc_lot === null) {
throw new Error('dc_lot can not be null')
}
}
toJson() {
debug('toJson')
return {
state: this.getState(),
text1: this.getText1(),
text1Meaning: this.getText1Meaning(),
text2: this.getText2(),
timestamp: this.getTimestamp(),
}
}
getId(): number {
return binding.dcn_lot_get_id(this.dc_lot)
}
getState(): number {
return binding.dcn_lot_get_state(this.dc_lot)
}
getText1(): string {
return binding.dcn_lot_get_text1(this.dc_lot)
}
getText1Meaning(): string {
return binding.dcn_lot_get_text1_meaning(this.dc_lot)
}
getText2(): string {
return binding.dcn_lot_get_text2(this.dc_lot)
}
getTimestamp(): number {
return binding.dcn_lot_get_timestamp(this.dc_lot)
}
}

370
node/lib/message.ts Normal file
View File

@@ -0,0 +1,370 @@
/* eslint-disable camelcase */
import binding from './binding'
import { C } from './constants'
import { Lot } from './lot'
import { Chat } from './chat'
import { WebxdcInfo } from './context'
const debug = require('debug')('deltachat:node:message')
export enum MessageDownloadState {
Available = C.DC_DOWNLOAD_AVAILABLE,
Done = C.DC_DOWNLOAD_DONE,
Failure = C.DC_DOWNLOAD_FAILURE,
InProgress = C.DC_DOWNLOAD_IN_PROGRESS,
}
/**
* Helper class for message states so you can do e.g.
*
* if (msg.getState().isPending()) { .. }
*
*/
export class MessageState {
constructor(public state: number) {
debug(`MessageState constructor ${state}`)
}
isUndefined() {
return this.state === C.DC_STATE_UNDEFINED
}
isFresh() {
return this.state === C.DC_STATE_IN_FRESH
}
isNoticed() {
return this.state === C.DC_STATE_IN_NOTICED
}
isSeen() {
return this.state === C.DC_STATE_IN_SEEN
}
isPending() {
return this.state === C.DC_STATE_OUT_PENDING
}
isFailed() {
return this.state === C.DC_STATE_OUT_FAILED
}
isDelivered() {
return this.state === C.DC_STATE_OUT_DELIVERED
}
isReceived() {
return this.state === C.DC_STATE_OUT_MDN_RCVD
}
}
/**
* Helper class for message types so you can do e.g.
*
* if (msg.getViewType().isVideo()) { .. }
*
*/
export class MessageViewType {
constructor(public viewType: number) {
debug(`MessageViewType constructor ${viewType}`)
}
isText() {
return this.viewType === C.DC_MSG_TEXT
}
isImage() {
return this.viewType === C.DC_MSG_IMAGE || this.viewType === C.DC_MSG_GIF
}
isGif() {
return this.viewType === C.DC_MSG_GIF
}
isAudio() {
return this.viewType === C.DC_MSG_AUDIO || this.viewType === C.DC_MSG_VOICE
}
isVoice() {
return this.viewType === C.DC_MSG_VOICE
}
isVideo() {
return this.viewType === C.DC_MSG_VIDEO
}
isFile() {
return this.viewType === C.DC_MSG_FILE
}
isVideochatInvitation() {
return this.viewType === C.DC_MSG_VIDEOCHAT_INVITATION
}
}
interface NativeMessage {}
/**
* Wrapper around dc_msg_t*
*/
export class Message {
constructor(public dc_msg: NativeMessage) {
debug('Message constructor')
if (dc_msg === null) {
throw new Error('dc_msg can not be null')
}
}
toJson() {
debug('toJson')
const quotedMessage = this.getQuotedMessage()
const viewType = binding.dcn_msg_get_viewtype(this.dc_msg)
return {
chatId: this.getChatId(),
webxdcInfo: viewType == C.DC_MSG_WEBXDC ? this.webxdcInfo : null,
downloadState: this.downloadState,
duration: this.getDuration(),
file: this.getFile(),
fromId: this.getFromId(),
id: this.getId(),
quotedText: this.getQuotedText(),
quotedMessageId: quotedMessage ? quotedMessage.getId() : null,
receivedTimestamp: this.getReceivedTimestamp(),
sortTimestamp: this.getSortTimestamp(),
text: this.getText(),
timestamp: this.getTimestamp(),
hasLocation: this.hasLocation(),
hasHTML: this.hasHTML,
viewType,
state: binding.dcn_msg_get_state(this.dc_msg),
hasDeviatingTimestamp: this.hasDeviatingTimestamp(),
showPadlock: this.getShowpadlock(),
summary: this.getSummary().toJson(),
subject: this.subject,
isSetupmessage: this.isSetupmessage(),
isInfo: this.isInfo(),
isForwarded: this.isForwarded(),
dimensions: {
height: this.getHeight(),
width: this.getWidth(),
},
videochatType: this.getVideochatType(),
videochatUrl: this.getVideochatUrl(),
overrideSenderName: this.overrideSenderName,
parentId: this.parent?.getId(),
}
}
getChatId(): number {
return binding.dcn_msg_get_chat_id(this.dc_msg)
}
get webxdcInfo(): WebxdcInfo | null {
let info = binding.dcn_msg_get_webxdc_info(this.dc_msg)
return info
? JSON.parse(binding.dcn_msg_get_webxdc_info(this.dc_msg))
: null
}
get downloadState(): MessageDownloadState {
return binding.dcn_msg_get_download_state(this.dc_msg)
}
get parent(): Message | null {
let msg = binding.dcn_msg_get_parent(this.dc_msg)
return msg ? new Message(msg) : null
}
getDuration(): number {
return binding.dcn_msg_get_duration(this.dc_msg)
}
getFile(): string {
return binding.dcn_msg_get_file(this.dc_msg)
}
getFilebytes(): number {
return binding.dcn_msg_get_filebytes(this.dc_msg)
}
getFilemime(): string {
return binding.dcn_msg_get_filemime(this.dc_msg)
}
getFilename(): string {
return binding.dcn_msg_get_filename(this.dc_msg)
}
getFromId(): number {
return binding.dcn_msg_get_from_id(this.dc_msg)
}
getHeight(): number {
return binding.dcn_msg_get_height(this.dc_msg)
}
getId(): number {
return binding.dcn_msg_get_id(this.dc_msg)
}
getQuotedText(): string {
return binding.dcn_msg_get_quoted_text(this.dc_msg)
}
getQuotedMessage(): Message | null {
const dc_msg = binding.dcn_msg_get_quoted_msg(this.dc_msg)
return dc_msg ? new Message(dc_msg) : null
}
getReceivedTimestamp(): number {
return binding.dcn_msg_get_received_timestamp(this.dc_msg)
}
getSetupcodebegin() {
return binding.dcn_msg_get_setupcodebegin(this.dc_msg)
}
getShowpadlock() {
return Boolean(binding.dcn_msg_get_showpadlock(this.dc_msg))
}
getSortTimestamp(): number {
return binding.dcn_msg_get_sort_timestamp(this.dc_msg)
}
getState() {
return new MessageState(binding.dcn_msg_get_state(this.dc_msg))
}
getSummary(chat?: Chat) {
const dc_chat = (chat && chat.dc_chat) || null
return new Lot(binding.dcn_msg_get_summary(this.dc_msg, dc_chat))
}
get subject(): string {
return binding.dcn_msg_get_subject(this.dc_msg)
}
getSummarytext(approxCharacters: number): string {
approxCharacters = approxCharacters || 0
return binding.dcn_msg_get_summarytext(this.dc_msg, approxCharacters)
}
getText(): string {
return binding.dcn_msg_get_text(this.dc_msg)
}
getTimestamp(): number {
return binding.dcn_msg_get_timestamp(this.dc_msg)
}
getViewType() {
return new MessageViewType(binding.dcn_msg_get_viewtype(this.dc_msg))
}
getVideochatType(): number {
return binding.dcn_msg_get_videochat_type(this.dc_msg)
}
getVideochatUrl(): string {
return binding.dcn_msg_get_videochat_url(this.dc_msg)
}
getWidth(): number {
return binding.dcn_msg_get_width(this.dc_msg)
}
get overrideSenderName(): string {
return binding.dcn_msg_get_override_sender_name(this.dc_msg)
}
hasDeviatingTimestamp() {
return binding.dcn_msg_has_deviating_timestamp(this.dc_msg)
}
hasLocation() {
return Boolean(binding.dcn_msg_has_location(this.dc_msg))
}
get hasHTML() {
return Boolean(binding.dcn_msg_has_html(this.dc_msg))
}
isDeadDrop() {
// TODO: Fix
//return this.getChatId() === C.DC_CHAT_ID_DEADDROP
return false
}
isForwarded() {
return Boolean(binding.dcn_msg_is_forwarded(this.dc_msg))
}
isIncreation() {
return Boolean(binding.dcn_msg_is_increation(this.dc_msg))
}
isInfo() {
return Boolean(binding.dcn_msg_is_info(this.dc_msg))
}
isSent() {
return Boolean(binding.dcn_msg_is_sent(this.dc_msg))
}
isSetupmessage() {
return Boolean(binding.dcn_msg_is_setupmessage(this.dc_msg))
}
latefilingMediasize(width: number, height: number, duration: number) {
binding.dcn_msg_latefiling_mediasize(this.dc_msg, width, height, duration)
}
setDimension(width: number, height: number) {
binding.dcn_msg_set_dimension(this.dc_msg, width, height)
return this
}
setDuration(duration: number) {
binding.dcn_msg_set_duration(this.dc_msg, duration)
return this
}
setFile(file: string, mime?: string) {
if (typeof file !== 'string') throw new Error('Missing filename')
binding.dcn_msg_set_file(this.dc_msg, file, mime || '')
return this
}
setLocation(longitude: number, latitude: number) {
binding.dcn_msg_set_location(this.dc_msg, longitude, latitude)
return this
}
setQuote(quotedMessage: Message | null) {
binding.dcn_msg_set_quote(this.dc_msg, quotedMessage?.dc_msg)
return this
}
setText(text: string) {
binding.dcn_msg_set_text(this.dc_msg, text)
return this
}
setHTML(html: string) {
binding.dcn_msg_set_html(this.dc_msg, html)
return this
}
setOverrideSenderName(senderName: string) {
binding.dcn_msg_set_override_sender_name(this.dc_msg, senderName)
return this
}
/** Force the message to be sent in plain text.
*
* This API is for bots, there is no need to expose it in the UI.
*/
forcePlaintext() {
binding.dcn_msg_force_plaintext(this.dc_msg)
}
}

24
node/lib/types.ts Normal file
View File

@@ -0,0 +1,24 @@
import { C } from './constants'
export type ChatTypes =
| C.DC_CHAT_TYPE_GROUP
| C.DC_CHAT_TYPE_MAILINGLIST
| C.DC_CHAT_TYPE_SINGLE
| C.DC_CHAT_TYPE_UNDEFINED
export interface ChatJSON {
archived: boolean
pinned: boolean
color: string
id: number
name: string
profileImage: string
type: number
isSelfTalk: boolean
isUnpromoted: boolean
isProtected: boolean
canSend: boolean
isDeviceTalk: boolean
isContactRequest: boolean
muted: boolean
}

6
node/lib/util.ts Normal file
View File

@@ -0,0 +1,6 @@
/**
* @param integerColor expects a 24bit rgb integer (left to right: 8bits red, 8bits green, 8bits blue)
*/
export function integerToHexColor(integerColor: number) {
return '#' + (integerColor + 16777216).toString(16).substring(1)
}

View File

@@ -0,0 +1,13 @@
diff --git i/node/binding.gyp w/node/binding.gyp
index b0d92eae..c5e504fa 100644
--- i/node/binding.gyp
+++ w/node/binding.gyp
@@ -43,7 +43,7 @@
"include_dirs": ["../deltachat-ffi"],
"ldflags": ["-Wl,-Bsymbolic"], # Prevent sqlite3 from electron from overriding sqlcipher
"libraries": [
- "../../target/release/libdeltachat.a",
+ "../../target/x86_64-apple-darwin/release/libdeltachat.a",
"-ldl",
],
"conditions": [],

26
node/scripts/common.js Normal file
View File

@@ -0,0 +1,26 @@
const spawnSync = require('child_process').spawnSync
const verbose = isVerbose()
function spawn (cmd, args, opts) {
log(`>> spawn: ${cmd} ${args.join(' ')}`)
const result = spawnSync(cmd, args, opts)
if (result.status === null) {
console.error(`Could not find ${cmd}`)
process.exit(1)
} else if (result.status !== 0) {
console.error(`${cmd} failed with code ${result.status}`)
process.exit(1)
}
}
function log (...args) {
if (verbose) console.log(...args)
}
function isVerbose () {
const loglevel = process.env.npm_config_loglevel
return loglevel === 'verbose' || process.env.CI === 'true'
}
module.exports = { spawn, log, isVerbose, verbose }

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
const split = require('split2')
const data = []
const regex = /^#define\s+(\w+)\s+(\w+)/i
const header = path.resolve(
__dirname,
'../../deltachat-ffi/deltachat.h'
)
console.log('Generating constants...')
fs.createReadStream(header)
.pipe(split())
.on('data', (line) => {
const match = regex.exec(line)
if (match) {
const key = match[1]
const value = parseInt(match[2])
if (isNaN(value)) return
data.push({ key, value })
}
})
.on('end', () => {
const constants = data
.filter(
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
)
.sort((lhs, rhs) => {
if (lhs.key < rhs.key) return -1
else if (lhs.key > rhs.key) return 1
return 0
})
.map((row) => {
return ` ${row.key}: ${row.value}`
})
.join(',\n')
const events = data
.sort((lhs, rhs) => {
if (lhs.value < rhs.value) return -1
else if (lhs.value > rhs.value) return 1
return 0
})
.filter((i) => {
return i.key.startsWith('DC_EVENT_')
})
.map((i) => {
return ` ${i.value}: '${i.key}'`
})
.join(',\n')
// backwards compat
fs.writeFileSync(
path.resolve(__dirname, '../constants.js'),
`// Generated!\n\nmodule.exports = {\n${constants}\n}\n`
)
// backwards compat
fs.writeFileSync(
path.resolve(__dirname, '../events.js'),
`/* eslint-disable quotes */\n// Generated!\n\nmodule.exports = {\n${events}\n}\n`
)
fs.writeFileSync(
path.resolve(__dirname, '../lib/constants.ts'),
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, ' =')},\n}\n
// Generated!\n\nexport const EventId2EventName: { [key: number]: string } = {\n${events},\n}\n`
)
})

22
node/scripts/install.js Normal file
View File

@@ -0,0 +1,22 @@
const {execSync} = require('child_process')
const {existsSync} = require('fs')
const {join} = require('path')
const run = (cmd) => {
console.log('[i] running `' + cmd + '`')
execSync(cmd, {stdio: 'inherit'})
}
// Build bindings
if (process.env.USE_SYSTEM_LIBDELTACHAT === 'true') {
console.log('[i] USE_SYSTEM_LIBDELTACHAT is true, rebuilding c bindings and using pkg-config to retrieve lib paths and cflags of libdeltachat')
run('npm run build:bindings:c:c')
} else {
console.log('[i] Building rust core & c bindings, if possible use prebuilds')
run('npm run install:prebuilds')
}
if (!existsSync(join(__dirname, '..', 'dist'))) {
console.log('[i] Didn\'t find already built typescript bindings. Trying to transpile them. If this fail, make sure typescript is installed ;)')
run('npm run build:bindings:ts')
}

View File

@@ -0,0 +1,46 @@
const { readFileSync } = require('fs')
const sha = JSON.parse(
readFileSync(process.env['GITHUB_EVENT_PATH'], 'utf8')
).pull_request.head.sha
const base_url =
'https://download.delta.chat/node/'
const GITHUB_API_URL =
'https://api.github.com/repos/deltachat/deltachat-core-rust/statuses/' + sha
const file_url = process.env['URL']
const GITHUB_TOKEN = process.env['GITHUB_TOKEN']
const STATUS_DATA = {
state: 'success',
description: '⏩ Click on "Details" to download →',
context: 'Download the node-bindings.tar.gz',
target_url: base_url + file_url,
}
const http = require('https')
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'github-action ci for deltachat deskop',
authorization: 'Bearer ' + GITHUB_TOKEN,
},
}
const req = http.request(GITHUB_API_URL, options, function(res) {
var chunks = []
res.on('data', function(chunk) {
chunks.push(chunk)
})
res.on('end', function() {
var body = Buffer.concat(chunks)
console.log(body.toString())
})
})
req.write(JSON.stringify(STATUS_DATA))
req.end()

View File

@@ -0,0 +1,57 @@
const fs = require('fs')
const path = require('path')
if (process.platform !== 'win32') {
console.log('postinstall: not windows, so skipping!')
process.exit(0)
}
const from = path.resolve(
__dirname,
'..',
'..',
'target',
'release',
'deltachat.dll'
)
const getDestination = () => {
const argv = process.argv
if (argv.length === 3 && argv[2] === '--prebuild') {
return path.resolve(
__dirname,
'..',
'prebuilds',
'win32-x64',
'deltachat.dll'
)
} else {
return path.resolve(
__dirname,
'..',
'build',
'Release',
'deltachat.dll'
)
}
}
const dest = getDestination()
copy(from, dest, (err) => {
if (err) throw err
console.log(`postinstall: copied ${from} to ${dest}`)
})
function copy (from, to, cb) {
fs.stat(from, (err, st) => {
if (err) return cb(err)
fs.readFile(from, (err, buf) => {
if (err) return cb(err)
fs.writeFile(to, buf, (err) => {
if (err) return cb(err)
fs.chmod(to, st.mode, cb)
})
})
})
}

View File

@@ -0,0 +1,17 @@
const path = require('path')
const { spawn } = require('./common')
const opts = {
cwd: path.resolve(__dirname, '../..'),
stdio: 'inherit'
}
const buildArgs = [
'build',
'--release',
'--features',
'vendored',
'-p',
'deltachat_ffi'
]
spawn('cargo', buildArgs, opts)

3505
node/src/module.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
#include <napi-macros.h>
#undef NAPI_STATUS_THROWS
#define NAPI_STATUS_THROWS(call) \
if ((call) != napi_ok) { \
napi_throw_error(env, NULL, #call " failed!"); \
}
#define NAPI_DCN_CONTEXT() \
dcn_context_t* dcn_context; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_context)); \
if (!dcn_context) { \
const char* msg = "Provided dnc_context is null"; \
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
} \
if (!dcn_context->dc_context) { \
const char* msg = "Provided dc_context is null, did you close the context or not open it?"; \
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
}
#define NAPI_DCN_ACCOUNTS() \
dcn_accounts_t* dcn_accounts; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_accounts)); \
if (!dcn_accounts) { \
const char* msg = "Provided dnc_acounts is null"; \
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
} \
if (!dcn_accounts->dc_accounts) { \
const char* msg = "Provided dc_accounts is null, did you unref the accounts object?"; \
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
}
#define NAPI_DC_CHAT() \
dc_chat_t* dc_chat; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_chat));
#define NAPI_DC_CHATLIST() \
dc_chatlist_t* dc_chatlist; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_chatlist));
#define NAPI_DC_CONTACT() \
dc_contact_t* dc_contact; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_contact));
#define NAPI_DC_LOT() \
dc_lot_t* dc_lot; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_lot));
#define NAPI_DC_MSG() \
dc_msg_t* dc_msg; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_msg));
#define NAPI_ARGV_DC_MSG(name, position) \
dc_msg_t* name; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[position], (void**)&name));
#define NAPI_DC_PROVIDER() \
dc_provider_t* dc_provider; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_provider));
#define NAPI_DC_ARRAY() \
dc_array_t* dc_array; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_array));
#define NAPI_RETURN_UNDEFINED() \
return 0;
#define NAPI_RETURN_UINT64(name) \
napi_value return_int64; \
NAPI_STATUS_THROWS(napi_create_bigint_int64(env, name, &return_int64)); \
return return_int64;
#define NAPI_RETURN_INT64(name) \
napi_value return_int64; \
NAPI_STATUS_THROWS(napi_create_int64(env, name, &return_int64)); \
return return_int64;
#define NAPI_RETURN_AND_UNREF_STRING(name) \
napi_value return_value; \
if (name == NULL) { \
NAPI_STATUS_THROWS(napi_get_null(env, &return_value)); \
return return_value; \
} \
NAPI_STATUS_THROWS(napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &return_value)); \
dc_str_unref(name); \
return return_value;
#define NAPI_ASYNC_CARRIER_BEGIN(name) \
typedef struct name##_carrier_t { \
napi_ref callback_ref; \
napi_async_work async_work; \
dcn_context_t* dcn_context;
#define NAPI_ASYNC_CARRIER_END(name) \
} name##_carrier_t;
#define NAPI_ASYNC_EXECUTE(name) \
static void name##_execute(napi_env env, void* data)
#define NAPI_ASYNC_GET_CARRIER(name) \
name##_carrier_t* carrier = (name##_carrier_t*)data;
#define NAPI_ASYNC_COMPLETE(name) \
static void name##_complete(napi_env env, napi_status status, void* data)
#define NAPI_ASYNC_CALL_AND_DELETE_CB() \
napi_value global; \
NAPI_STATUS_THROWS(napi_get_global(env, &global)); \
napi_value callback; \
NAPI_STATUS_THROWS(napi_get_reference_value(env, carrier->callback_ref, &callback)); \
NAPI_STATUS_THROWS(napi_call_function(env, global, callback, argc, argv, NULL)); \
NAPI_STATUS_THROWS(napi_delete_reference(env, carrier->callback_ref)); \
NAPI_STATUS_THROWS(napi_delete_async_work(env, carrier->async_work));
#define NAPI_ASYNC_NEW_CARRIER(name) \
name##_carrier_t* carrier = calloc(1, sizeof(name##_carrier_t)); \
carrier->dcn_context = dcn_context;
#define NAPI_ASYNC_QUEUE_WORK(name, cb) \
napi_value callback = cb; \
napi_value async_resource_name; \
NAPI_STATUS_THROWS(napi_create_reference(env, callback, 1, &carrier->callback_ref)); \
NAPI_STATUS_THROWS(napi_create_string_utf8(env, #name "_callback", \
NAPI_AUTO_LENGTH, \
&async_resource_name)); \
NAPI_STATUS_THROWS(napi_create_async_work(env, callback, async_resource_name, \
name##_execute, name##_complete, \
carrier, &carrier->async_work)); \
NAPI_STATUS_THROWS(napi_queue_async_work(env, carrier->async_work));
/*** this could/should be moved to napi-macros ***/
#define NAPI_DOUBLE(name, val) \
double name; \
if (napi_get_value_double(env, val, &name) != napi_ok) { \
napi_throw_error(env, "EINVAL", "Expected double"); \
return NULL; \
}
#define NAPI_ARGV_DOUBLE(name, i) \
NAPI_DOUBLE(name, argv[i])

BIN
node/test/fixtures/avatar.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
node/test/fixtures/image.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
node/test/fixtures/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

862
node/test/test.js Normal file
View File

@@ -0,0 +1,862 @@
// @ts-check
import DeltaChat, { Message } from '../dist'
import binding from '../binding'
import { strictEqual } from 'assert'
import chai, { expect } from 'chai'
import chaiAsPromised from 'chai-as-promised'
import { EventId2EventName, C } from '../dist/constants'
import { join } from 'path'
import { mkdtempSync, statSync } from 'fs'
import { tmpdir } from 'os'
import { Context } from '../dist/context'
chai.use(chaiAsPromised)
async function createTempUser(url) {
const fetch = require('node-fetch')
async function postData(url = '') {
// Default options are marked with *
const response = await fetch(url, {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, *same-origin, omit
headers: {
'cache-control': 'no-cache',
},
referrerPolicy: 'no-referrer', // no-referrer, *client
})
return response.json() // parses JSON response into native JavaScript objects
}
return await postData(url)
}
describe('static tests', function () {
it('reverse lookup of events', function () {
const eventKeys = Object.keys(EventId2EventName).map((k) => Number(k))
const eventValues = Object.values(EventId2EventName)
const reverse = eventValues.map((v) => C[v])
expect(reverse).to.be.deep.equal(eventKeys)
})
it('event constants are consistent', function () {
const eventKeys = Object.keys(C)
.filter((k) => k.startsWith('DC_EVENT_'))
.sort()
const eventValues = Object.values(EventId2EventName).sort()
expect(eventKeys).to.be.deep.equal(eventValues)
})
it('static method maybeValidAddr()', function () {
expect(DeltaChat.maybeValidAddr(null)).to.equal(false)
expect(DeltaChat.maybeValidAddr('')).to.equal(false)
expect(DeltaChat.maybeValidAddr('uuu')).to.equal(false)
expect(DeltaChat.maybeValidAddr('dd.tt')).to.equal(false)
expect(DeltaChat.maybeValidAddr('tt.dd@yggmail')).to.equal(true)
expect(DeltaChat.maybeValidAddr('u@d')).to.equal(true)
//expect(DeltaChat.maybeValidAddr('u@d.')).to.equal(false)
//expect(DeltaChat.maybeValidAddr('u@d.t')).to.equal(false)
//expect(DeltaChat.maybeValidAddr('u@.tt')).to.equal(false)
expect(DeltaChat.maybeValidAddr('@d.tt')).to.equal(false)
expect(DeltaChat.maybeValidAddr('user@domain.tld')).to.equal(true)
expect(DeltaChat.maybeValidAddr('u@d.tt')).to.equal(true)
})
it('static getSystemInfo()', function () {
const info = Context.getSystemInfo()
expect(info).to.contain.keys([
'arch',
'deltachat_core_version',
'sqlite_version',
])
})
it('static context.getProviderFromEmail("example@example.com")', function () {
const provider = DeltaChat.getProviderFromEmail('example@example.com')
expect(provider).to.deep.equal({
before_login_hint: "Hush this provider doesn't exist!",
overview_page: 'https://providers.delta.chat/example-com',
status: 3,
})
})
})
describe('Basic offline Tests', function () {
it('opens a context', async function () {
const { dc, context } = DeltaChat.newTemporary()
strictEqual(context.isConfigured(), false)
dc.close()
})
it('set config', async function () {
const { dc, context } = DeltaChat.newTemporary()
context.setConfig('bot', true)
strictEqual(context.getConfig('bot'), '1')
context.setConfig('bot', false)
strictEqual(context.getConfig('bot'), '0')
context.setConfig('bot', '1')
strictEqual(context.getConfig('bot'), '1')
context.setConfig('bot', '0')
strictEqual(context.getConfig('bot'), '0')
context.setConfig('bot', 1)
strictEqual(context.getConfig('bot'), '1')
context.setConfig('bot', 0)
strictEqual(context.getConfig('bot'), '0')
context.setConfig('bot', null)
strictEqual(context.getConfig('bot'), '')
strictEqual(context.getConfig('selfstatus'), '')
context.setConfig('selfstatus', 'hello')
strictEqual(context.getConfig('selfstatus'), 'hello')
context.setConfig('selfstatus', '')
strictEqual(context.getConfig('selfstatus'), '')
context.setConfig('selfstatus', null)
strictEqual(context.getConfig('selfstatus'), '')
dc.close()
})
it('configure with either missing addr or missing mail_pw throws', async function () {
const { dc, context } = DeltaChat.newTemporary()
dc.startEvents()
await expect(
context.configure({ addr: 'delta1@delta.localhost' })
).to.eventually.be.rejectedWith('Please enter a password.')
await expect(context.configure({ mailPw: 'delta1' })).to.eventually.be
.rejected
context.stopOngoingProcess()
dc.close()
})
it('context.getInfo()', async function () {
const { dc, context } = DeltaChat.newTemporary()
const info = await context.getInfo()
expect(typeof info).to.be.equal('object')
expect(info).to.contain.keys([
'arch',
'bcc_self',
'blobdir',
'bot',
'configured_mvbox_folder',
'configured_sentbox_folder',
'database_dir',
'database_encrypted',
'database_version',
'delete_device_after',
'delete_server_after',
'deltachat_core_version',
'display_name',
'download_limit',
'e2ee_enabled',
'entered_account_settings',
'fetch_existing_msgs',
'fingerprint',
'folders_configured',
'is_configured',
'journal_mode',
'key_gen_type',
'last_housekeeping',
'level',
'mdns_enabled',
'media_quality',
'messages_in_contact_requests',
'mvbox_move',
'num_cpus',
'number_of_chat_messages',
'number_of_chats',
'number_of_contacts',
'only_fetch_mvbox',
'private_key_count',
'public_key_count',
'quota_exceeding',
'scan_all_folders_debounce_secs',
'selfavatar',
'send_sync_msgs',
'sentbox_watch',
'show_emails',
'socks5_enabled',
'sqlite_version',
'uptime',
'used_account_settings',
'webrtc_instance',
])
dc.close()
})
})
describe('Offline Tests with unconfigured account', function () {
let [dc, context, accountId, directory] = [null, null, null, null]
this.beforeEach(async function () {
let tmp = DeltaChat.newTemporary()
dc = tmp.dc
context = tmp.context
accountId = tmp.accountId
directory = tmp.directory
dc.startEvents()
})
this.afterEach(async function () {
if (context) {
context.stopOngoingProcess()
}
if (dc) {
try {
dc.stopIO()
dc.close()
} catch (error) {
console.error(error)
}
}
dc = null
context = null
accountId = null
directory = null
})
it('invalid context.joinSecurejoin', async function () {
expect(context.joinSecurejoin('test')).to.be.eq(0)
})
it('Device Chat', async function () {
const deviceChatMessageText = 'test234'
expect((await context.getChatList(0, '', null)).getCount()).to.equal(
0,
'no device chat after setup'
)
await context.addDeviceMessage('test', deviceChatMessageText)
const chatList = await context.getChatList(0, '', null)
expect(chatList.getCount()).to.equal(
1,
'device chat after adding device msg'
)
const deviceChatId = await chatList.getChatId(0)
const deviceChat = await context.getChat(deviceChatId)
expect(deviceChat.isDeviceTalk()).to.be.true
expect(deviceChat.toJson().isDeviceTalk).to.be.true
const deviceChatMessages = await context.getChatMessages(deviceChatId, 0, 0)
expect(deviceChatMessages.length).to.be.equal(
1,
'device chat has added message'
)
const deviceChatMessage = await context.getMessage(deviceChatMessages[0])
expect(deviceChatMessage.getText()).to.equal(
deviceChatMessageText,
'device chat message has the inserted text'
)
})
it('should have e2ee enabled and right blobdir', function () {
expect(context.getConfig('e2ee_enabled')).to.equal(
'1',
'e2eeEnabled correct'
)
expect(
String(context.getBlobdir()).startsWith(directory),
'blobdir should be inside temp directory'
)
expect(
String(context.getBlobdir()).endsWith('db.sqlite-blobs'),
'blobdir end with "db.sqlite-blobs"'
)
})
it('should create chat from contact and Chat methods', async function () {
const contactId = context.createContact('aaa', 'aaa@site.org')
strictEqual(context.lookupContactIdByAddr('aaa@site.org'), contactId)
strictEqual(context.lookupContactIdByAddr('nope@site.net'), 0)
let chatId = context.createChatByContactId(contactId)
let chat = context.getChat(chatId)
strictEqual(
chat.getVisibility(),
C.DC_CHAT_VISIBILITY_NORMAL,
'not archived'
)
strictEqual(chat.getId(), chatId, 'chat id matches')
strictEqual(chat.getName(), 'aaa', 'chat name matches')
strictEqual(chat.getProfileImage(), null, 'no profile image')
strictEqual(chat.getType(), C.DC_CHAT_TYPE_SINGLE, 'single chat')
strictEqual(chat.isSelfTalk(), false, 'no self talk')
// TODO make sure this is really the case!
strictEqual(chat.isUnpromoted(), false, 'not unpromoted')
strictEqual(chat.isProtected(), false, 'not verified')
strictEqual(typeof chat.color, 'string', 'color is a string')
strictEqual(context.getDraft(chatId), null, 'no draft message')
context.setDraft(chatId, context.messageNew().setText('w00t!'))
strictEqual(
context.getDraft(chatId).toJson().text,
'w00t!',
'draft text correct'
)
context.setDraft(chatId, null)
strictEqual(context.getDraft(chatId), null, 'draft removed')
strictEqual(context.getChatIdByContactId(contactId), chatId)
expect(context.getChatContacts(chatId)).to.deep.equal([contactId])
context.setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_ARCHIVED)
strictEqual(
context.getChat(chatId).getVisibility(),
C.DC_CHAT_VISIBILITY_ARCHIVED,
'chat archived'
)
context.setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_NORMAL)
strictEqual(
chat.getVisibility(),
C.DC_CHAT_VISIBILITY_NORMAL,
'chat unarchived'
)
chatId = context.createGroupChat('unverified group', false)
chat = context.getChat(chatId)
strictEqual(chat.isProtected(), false, 'is not verified')
strictEqual(chat.getType(), C.DC_CHAT_TYPE_GROUP, 'group chat')
expect(context.getChatContacts(chatId)).to.deep.equal([
C.DC_CONTACT_ID_SELF,
])
const draft2 = context.getDraft(chatId)
expect(draft2 == null, 'unpromoted group has no draft by default')
context.setChatName(chatId, 'NEW NAME')
strictEqual(context.getChat(chatId).getName(), 'NEW NAME', 'name updated')
chatId = context.createGroupChat('a verified group', true)
chat = context.getChat(chatId)
strictEqual(chat.isProtected(), true, 'is verified')
})
it('test setting profile image', async function () {
const chatId = context.createGroupChat('testing profile image group', false)
const image = 'image.jpeg'
const imagePath = join(__dirname, 'fixtures', image)
const blobs = context.getBlobdir()
context.setChatProfileImage(chatId, imagePath)
const blobPath = context.getChat(chatId).getProfileImage()
expect(blobPath.startsWith(blobs)).to.be.true
expect(blobPath.endsWith(image)).to.be.true
context.setChatProfileImage(chatId, null)
expect(context.getChat(chatId).getProfileImage()).to.be.equal(
null,
'image is null'
)
})
it('test setting ephemeral timer', function () {
const chatId = context.createGroupChat('testing ephemeral timer')
strictEqual(
context.getChatEphemeralTimer(chatId),
0,
'ephemeral timer is not set by default'
)
context.setChatEphemeralTimer(chatId, 60)
strictEqual(
context.getChatEphemeralTimer(chatId),
60,
'ephemeral timer is set to 1 minute'
)
context.setChatEphemeralTimer(chatId, 0)
strictEqual(
context.getChatEphemeralTimer(chatId),
0,
'ephemeral timer is reset'
)
})
it('should create and delete chat', function () {
const chatId = context.createGroupChat('GROUPCHAT')
const chat = context.getChat(chatId)
strictEqual(chat.getId(), chatId, 'correct chatId')
context.deleteChat(chat.getId())
strictEqual(context.getChat(chatId), null, 'chat removed')
})
it('new message and Message methods', function () {
const text = 'w00t!'
const msg = context.messageNew().setText(text)
strictEqual(msg.getChatId(), 0, 'chat id 0 before sent')
strictEqual(msg.getDuration(), 0, 'duration 0 before sent')
strictEqual(msg.getFile(), '', 'no file set by default')
strictEqual(msg.getFilebytes(), 0, 'and file bytes is 0')
strictEqual(msg.getFilemime(), '', 'no filemime by default')
strictEqual(msg.getFilename(), '', 'no filename set by default')
strictEqual(msg.getFromId(), 0, 'no contact id set by default')
strictEqual(msg.getHeight(), 0, 'plain text message have height 0')
strictEqual(msg.getId(), 0, 'id 0 before sent')
strictEqual(msg.getSetupcodebegin(), '', 'no setupcode begin')
strictEqual(msg.getShowpadlock(), false, 'no padlock by default')
const state = msg.getState()
strictEqual(state.isUndefined(), true, 'no state by default')
strictEqual(state.isFresh(), false, 'no state by default')
strictEqual(state.isNoticed(), false, 'no state by default')
strictEqual(state.isSeen(), false, 'no state by default')
strictEqual(state.isPending(), false, 'no state by default')
strictEqual(state.isFailed(), false, 'no state by default')
strictEqual(state.isDelivered(), false, 'no state by default')
strictEqual(state.isReceived(), false, 'no state by default')
const summary = msg.getSummary()
strictEqual(summary.getId(), 0, 'no summary id')
strictEqual(summary.getState(), 0, 'no summary state')
strictEqual(summary.getText1(), null, 'no summary text1')
strictEqual(summary.getText1Meaning(), 0, 'no summary text1 meaning')
strictEqual(summary.getText2(), '', 'no summary text2')
strictEqual(summary.getTimestamp(), 0, 'no summary timestamp')
//strictEqual(msg.getSummarytext(50), text, 'summary text is text')
strictEqual(msg.getText(), text, 'msg text set correctly')
strictEqual(msg.getTimestamp(), 0, 'no timestamp')
const viewType = msg.getViewType()
strictEqual(viewType.isText(), true)
strictEqual(viewType.isImage(), false)
strictEqual(viewType.isGif(), false)
strictEqual(viewType.isAudio(), false)
strictEqual(viewType.isVoice(), false)
strictEqual(viewType.isVideo(), false)
strictEqual(viewType.isFile(), false)
strictEqual(msg.getWidth(), 0, 'no message width')
strictEqual(msg.isDeadDrop(), false, 'not deaddrop')
strictEqual(msg.isForwarded(), false, 'not forwarded')
strictEqual(msg.isIncreation(), false, 'not in creation')
strictEqual(msg.isInfo(), false, 'not an info message')
strictEqual(msg.isSent(), false, 'messge is not sent')
strictEqual(msg.isSetupmessage(), false, 'not an autocrypt setup message')
msg.latefilingMediasize(10, 20, 30)
strictEqual(msg.getWidth(), 10, 'message width set correctly')
strictEqual(msg.getHeight(), 20, 'message height set correctly')
strictEqual(msg.getDuration(), 30, 'message duration set correctly')
msg.setDimension(100, 200)
strictEqual(msg.getWidth(), 100, 'message width set correctly')
strictEqual(msg.getHeight(), 200, 'message height set correctly')
msg.setDuration(314)
strictEqual(msg.getDuration(), 314, 'message duration set correctly')
expect(() => {
msg.setFile(null)
}).to.throw('Missing filename')
const logo = join(__dirname, 'fixtures', 'logo.png')
const stat = statSync(logo)
msg.setFile(logo)
strictEqual(msg.getFilebytes(), stat.size, 'correct file size')
strictEqual(msg.getFile(), logo, 'correct file name')
strictEqual(msg.getFilemime(), 'image/png', 'mime set implicitly')
msg.setFile(logo, 'image/gif')
strictEqual(msg.getFilemime(), 'image/gif', 'mime set (in)correctly')
msg.setFile(logo, 'image/png')
strictEqual(msg.getFilemime(), 'image/png', 'mime set correctly')
const json = msg.toJson()
expect(json).to.not.equal(null, 'not null')
strictEqual(typeof json, 'object', 'json object')
})
it('Contact methods', function () {
const contactId = context.createContact('First Last', 'first.last@site.org')
const contact = context.getContact(contactId)
strictEqual(contact.getAddress(), 'first.last@site.org', 'correct address')
strictEqual(typeof contact.color, 'string', 'color is a string')
strictEqual(contact.getDisplayName(), 'First Last', 'correct display name')
strictEqual(contact.getId(), contactId, 'contact id matches')
strictEqual(contact.getName(), 'First Last', 'correct name')
strictEqual(contact.getNameAndAddress(), 'First Last (first.last@site.org)')
strictEqual(contact.getProfileImage(), null, 'no contact image')
strictEqual(contact.isBlocked(), false, 'not blocked')
strictEqual(contact.isVerified(), false, 'unverified status')
strictEqual(contact.lastSeen, 0, 'last seen unknown')
})
it('create contacts from address book', function () {
const addresses = [
'Name One',
'name1@site.org',
'Name Two',
'name2@site.org',
'Name Three',
'name3@site.org',
]
const count = context.addAddressBook(addresses.join('\n'))
strictEqual(count, addresses.length / 2)
context
.getContacts(0, 'Name ')
.map((id) => context.getContact(id))
.forEach((contact) => {
expect(contact.getName().startsWith('Name ')).to.be.true
})
})
it('delete contacts', function () {
const id = context.createContact('someuser', 'someuser@site.com')
const contact = context.getContact(id)
strictEqual(contact.getId(), id, 'contact id matches')
strictEqual(context.deleteContact(id), true, 'delete call succesful')
strictEqual(context.getContact(id), null, 'contact is gone')
})
it('adding and removing a contact from a chat', function () {
const chatId = context.createGroupChat('adding_and_removing')
const contactId = context.createContact('Add Remove', 'add.remove@site.com')
strictEqual(
context.addContactToChat(chatId, contactId),
true,
'contact added'
)
strictEqual(
context.isContactInChat(chatId, contactId),
true,
'contact in chat'
)
strictEqual(
context.removeContactFromChat(chatId, contactId),
true,
'contact removed'
)
strictEqual(
context.isContactInChat(chatId, contactId),
false,
'contact not in chat'
)
})
it('blocking contacts', function () {
const id = context.createContact('badcontact', 'bad@site.com')
strictEqual(context.getBlockedCount(), 0)
strictEqual(context.getContact(id).isBlocked(), false)
expect(context.getBlockedContacts()).to.be.empty
context.blockContact(id, true)
strictEqual(context.getBlockedCount(), 1)
strictEqual(context.getContact(id).isBlocked(), true)
expect(context.getBlockedContacts()).to.deep.equal([id])
context.blockContact(id, false)
strictEqual(context.getBlockedCount(), 0)
strictEqual(context.getContact(id).isBlocked(), false)
expect(context.getBlockedContacts()).to.be.empty
})
it('ChatList methods', function () {
const ids = [
context.createGroupChat('groupchat1'),
context.createGroupChat('groupchat11'),
context.createGroupChat('groupchat111'),
]
let chatList = context.getChatList(0, 'groupchat1', null)
strictEqual(chatList.getCount(), 3, 'should contain above chats')
expect(ids.indexOf(chatList.getChatId(0))).not.to.equal(-1)
expect(ids.indexOf(chatList.getChatId(1))).not.to.equal(-1)
expect(ids.indexOf(chatList.getChatId(2))).not.to.equal(-1)
const lot = chatList.getSummary(0)
strictEqual(lot.getId(), 0, 'lot has no id')
strictEqual(lot.getState(), C.DC_STATE_UNDEFINED, 'correct state')
const text = 'No messages.'
context.createGroupChat('groupchat1111')
chatList = context.getChatList(0, 'groupchat1111', null)
strictEqual(
chatList.getSummary(0).getText2(),
text,
'custom new group message'
)
context.setChatVisibility(ids[0], C.DC_CHAT_VISIBILITY_ARCHIVED)
chatList = context.getChatList(C.DC_GCL_ARCHIVED_ONLY, 'groupchat1', null)
strictEqual(chatList.getCount(), 1, 'only one archived')
})
it('Remove qoute from (draft) message', function () {
context.addDeviceMessage('test_qoute', 'test')
const msgId = context.getChatMessages(10, 0, 0)[0]
const msg = context.messageNew()
msg.setQuote(context.getMessage(msgId))
expect(msg.getQuotedMessage()).to.not.be.null
msg.setQuote(null)
expect(msg.getQuotedMessage()).to.be.null
})
})
describe('Integration tests', function () {
this.timeout(60 * 3000) // increase timeout to 1min
let [dc, context, accountId, directory, account] = [
null,
null,
null,
null,
null,
]
let [dc2, context2, accountId2, directory2, account2] = [
null,
null,
null,
null,
null,
]
this.beforeEach(async function () {
let tmp = DeltaChat.newTemporary()
dc = tmp.dc
context = tmp.context
accountId = tmp.accountId
directory = tmp.directory
dc.startEvents()
})
this.afterEach(async function () {
if (context) {
try {
context.stopOngoingProcess()
} catch (error) {
console.error(error)
}
}
if (context2) {
try {
context2.stopOngoingProcess()
} catch (error) {
console.error(error)
}
}
if (dc) {
try {
dc.stopIO()
dc.close()
} catch (error) {
console.error(error)
}
}
dc = null
context = null
accountId = null
directory = null
context2 = null
accountId2 = null
directory2 = null
})
this.beforeAll(async function () {
if (!process.env.DCC_NEW_TMP_EMAIL) {
console.log(
'Missing DCC_NEW_TMP_EMAIL environment variable!, skip intergration tests'
)
this.skip()
}
account = await createTempUser(process.env.DCC_NEW_TMP_EMAIL)
if (!account || !account.email || !account.password) {
console.log(
"We didn't got back an account from the api, skip intergration tests"
)
this.skip()
}
})
it('configure', async function () {
strictEqual(context.isConfigured(), false, 'should not be configured')
// Not sure what's the best way to check the events
// TODO: check the events
// dc.once('DC_EVENT_CONFIGURE_PROGRESS', (data) => {
// t.pass('DC_EVENT_CONFIGURE_PROGRESS called at least once')
// })
// dc.on('DC_EVENT_ERROR', (error) => {
// console.error('DC_EVENT_ERROR', error)
// })
// dc.on('DC_EVENT_ERROR_NETWORK', (first, error) => {
// console.error('DC_EVENT_ERROR_NETWORK', error)
// })
// dc.on('ALL', (event, data1, data2) => console.log('ALL', event, data1, data2))
await expect(
context.configure({
addr: account.email,
mail_pw: account.password,
displayname: 'Delta One',
selfstatus: 'From Delta One with <3',
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
})
).to.be.eventually.fulfilled
strictEqual(context.getConfig('addr'), account.email, 'addr correct')
strictEqual(
context.getConfig('displayname'),
'Delta One',
'displayName correct'
)
strictEqual(
context.getConfig('selfstatus'),
'From Delta One with <3',
'selfStatus correct'
)
expect(
context.getConfig('selfavatar').endsWith('avatar.png'),
'selfavatar correct'
)
strictEqual(context.getConfig('e2ee_enabled'), '1', 'e2ee_enabled correct')
strictEqual(
context.getConfig('save_mime_headers'),
'',
'save_mime_headers correct'
)
expect(context.getBlobdir().endsWith('db.sqlite-blobs'), 'correct blobdir')
strictEqual(context.isConfigured(), true, 'is configured')
// whole re-configure to only change displayname: what the heck? (copied this from the old test)
await expect(
context.configure({
addr: account.email,
mail_pw: account.password,
displayname: 'Delta Two',
selfstatus: 'From Delta One with <3',
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
})
).to.be.eventually.fulfilled
strictEqual(
context.getConfig('displayname'),
'Delta Two',
'updated displayName correct'
)
})
it('Autocrypt setup - key transfer', async function () {
// Spawn a second dc instance with same account
// dc.on('ALL', (event, data1, data2) =>
// console.log('FIRST ', event, data1, data2)
// )
dc.stopIO()
await expect(
context.configure({
addr: account.email,
mail_pw: account.password,
displayname: 'Delta One',
selfstatus: 'From Delta One with <3',
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
})
).to.be.eventually.fulfilled
const accountId2 = dc.addAccount()
console.log('accountId2:', accountId2)
context2 = dc.accountContext(accountId2)
let setupCode = null
const waitForSetupCode = waitForSomething()
const waitForEnd = waitForSomething()
dc.on('ALL', (event, accountId, data1, data2) => {
console.log('[' + accountId + ']', event, data1, data2)
})
dc.on('DC_EVENT_MSGS_CHANGED', async (aId, chatId, msgId) => {
console.log('[' + accountId + '] DC_EVENT_MSGS_CHANGED', chatId, msgId)
if (
aId != accountId ||
!context.getChat(chatId).isSelfTalk() ||
!context.getMessage(msgId).isSetupmessage()
) {
return
}
console.log('Setupcode!')
let setupCode = await waitForSetupCode.promise
// console.log('incoming msg', { setupCode })
const messages = context.getChatMessages(chatId, 0, 0)
expect(messages.indexOf(msgId) !== -1, 'msgId is in chat messages').to.be
.true
const result = await context.continueKeyTransfer(msgId, setupCode)
expect(result === true, 'continueKeyTransfer was successful').to.be.true
waitForEnd.done()
})
dc.stopIO()
await expect(
context2.configure({
addr: account.email,
mail_pw: account.password,
displayname: 'Delta One',
selfstatus: 'From Delta One with <3',
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
})
).to.be.eventually.fulfilled
dc.startIO()
console.log('Sending autocrypt setup code')
setupCode = await context2.initiateKeyTransfer()
console.log('Sent autocrypt setup code')
waitForSetupCode.done(setupCode)
console.log('setupCode is: ' + setupCode)
expect(typeof setupCode).to.equal('string', 'setupCode is string')
await waitForEnd.promise
})
it('configure using invalid password should fail', async function () {
await expect(
context.configure({
addr: 'hpk5@testrun.org',
mail_pw: 'asd',
})
).to.be.eventually.rejected
})
})
/**
* @returns {{done: (result?)=>void, promise:Promise<any> }}
*/
function waitForSomething() {
let resolvePromise
const promise = new Promise((res, rej) => {
resolvePromise = res
})
return {
done: resolvePromise,
promise,
}
}

22
node/tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"outDir": "dist",
"rootDir": "./lib",
"sourceMap": true,
"module": "commonjs",
"target": "es5",
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"strictNullChecks": true,
"strict": true
},
"exclude": ["node_modules", "deltachat-core-rust", "dist", "scripts"],
"typedocOptions": {
"mode": "file",
"out": "docs",
"excludeNotExported": true,
"defaultCategory": "index",
"includeVersion": true
}
}

37
node/windows.md Normal file
View File

@@ -0,0 +1,37 @@
> Steps on how to get windows set up properly for the node bindings
## install git
E.g via <https://git-scm.com/download/win>
## install node
Download and install `v16` from <https://nodejs.org/en/>
## install rust
Download and run `rust-init.exe` from <https://www.rust-lang.org/tools/install>
## configure node for native addons
```
$ npm i node-gyp -g
$ npm i windows-build-tools -g
```
`windows-build-tools` will install `Visual Studio 2017` by default and should not mess with existing installations of `Visual Studio C++`.
## get the code
```
$ mkdir -p src/deltachat
$ cd src/deltachat
$ git clone https://github.com/deltachat/deltachat-node
```
## build the code
```
$ cd src/deltachat/deltachat-node
$ npm install
```

65
package.json Normal file
View File

@@ -0,0 +1,65 @@
{
"dependencies": {
"debug": "^4.1.1",
"napi-macros": "^2.0.0",
"node-gyp-build": "^4.1.0",
"split2": "^3.1.1"
},
"description": "node.js bindings for deltachat-core",
"devDependencies": {
"@types/debug": "^4.1.7",
"@types/node": "^16.11.26",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"esm": "^3.2.25",
"hallmark": "^2.0.0",
"mocha": "^8.2.1",
"node-fetch": "^2.6.7",
"node-gyp": "^9.0.0",
"opn-cli": "^5.0.0",
"prebuildify": "^3.0.0",
"prebuildify-ci": "^1.0.4",
"prettier": "^2.0.5",
"typedoc": "^0.17.0",
"typescript": "^3.9.10"
},
"engines": {
"node": ">=16.0.0"
},
"files": [
"node/scripts/*",
"*"
],
"homepage": "https://github.com/deltachat/deltachat-core-rust/tree/master/node",
"license": "GPL-3.0-or-later",
"main": "node/dist/index.js",
"name": "deltachat-node",
"repository": {
"type": "git",
"url": "https://github.com/deltachat/deltachat-core-rust.git"
},
"scripts": {
"build": "npm run build:core && npm run build:bindings",
"build:bindings": "npm run build:bindings:c && npm run build:bindings:ts",
"build:bindings:c": "npm run build:bindings:c:c && npm run build:bindings:c:postinstall",
"build:bindings:c:c": "cd node && npx node-gyp rebuild",
"build:bindings:c:postinstall": "node node/scripts/postinstall.js",
"build:bindings:ts": "cd node && tsc",
"build:core": "npm run build:core:rust && npm run build:core:constants",
"build:core:constants": "node node/scripts/generate-constants.js",
"build:core:rust": "node node/scripts/rebuild-core.js",
"clean": "rm -rf node/dist node/build node/prebuilds node/node_modules ./target",
"download-prebuilds": "prebuildify-ci download",
"hallmark": "hallmark --fix",
"install": "node node/scripts/install.js",
"install:prebuilds": "cd node && node-gyp-build \"npm run build:core\" \"npm run build:bindings:c:postinstall\"",
"lint": "prettier --check \"node/lib/**/*.{ts,tsx}\"",
"lint-fix": "prettier --write \"node/lib/**/*.{ts,tsx}\" \"node/test/**/*.js\"",
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
"test": "npm run test:lint && npm run test:mocha",
"test:lint": "npm run lint",
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec"
},
"types": "node/dist/index.d.ts",
"version": "1.84.0"
}

View File

@@ -22,7 +22,7 @@ def test_echo_quit_plugin(acfactory, lp):
botproc = acfactory.run_bot_process(echo_and_quit)
lp.sec("creating a temp account to contact the bot")
ac1 = acfactory.get_one_online_account()
ac1, = acfactory.get_online_accounts(1)
lp.sec("sending a message to the bot")
bot_contact = ac1.create_contact(botproc.addr)
@@ -42,7 +42,7 @@ def test_group_tracking_plugin(acfactory, lp):
lp.sec("creating one group-tracking bot and two temp accounts")
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
ac1, ac2 = acfactory.get_two_online_accounts(quiet=True)
ac1, ac2 = acfactory.get_online_accounts(2)
botproc.fnmatch_lines("""
*ac_configure_completed*

View File

@@ -24,7 +24,7 @@ if __name__ == "__main__":
print("running:", " ".join(cmd))
subprocess.check_call(cmd)
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
subprocess.check_call("rm -rf build/ src/deltachat/*.so src/deltachat/*.dylib src/deltachat/*.dll" , shell=True)
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
subprocess.check_call([

View File

@@ -1,5 +1,8 @@
[mypy]
[mypy-py.*]
ignore_missing_imports = True
[mypy-deltachat.capi.*]
ignore_missing_imports = True
@@ -17,3 +20,7 @@ ignore_missing_imports = True
[mypy-_pytest.*]
ignore_missing_imports = True
[mypy-imap_tools.*]
ignore_missing_imports = True

View File

@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0"]
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0", "pkgconfig"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]

View File

@@ -11,8 +11,11 @@ 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', 'requests'],
setup_requires=['setuptools_scm'], # required for compatibility with `python3 setup.py sdist`
install_requires=['cffi>=1.0.0', 'pluggy', 'imap-tools', 'requests'],
setup_requires=[
'setuptools_scm', # required for compatibility with `python3 setup.py sdist`
'pkgconfig',
],
packages=setuptools.find_packages('src'),
package_dir={'': 'src'},
cffi_modules=['src/deltachat/_build.py:ffibuilder'],

View File

@@ -2,7 +2,7 @@ import sys
from . import capi, const, hookspec # noqa
from .capi import ffi # noqa
from .account import Account # noqa
from .account import Account, get_core_info # noqa
from .message import Message # noqa
from .contact import Contact # noqa
from .chat import Chat # noqa

View File

@@ -8,9 +8,9 @@ import shutil
import subprocess
import tempfile
import textwrap
import types
import cffi
import pkgconfig # type: ignore
def local_build_flags(projdir, target):
@@ -19,36 +19,31 @@ def local_build_flags(projdir, target):
:param projdir: The root directory of the deltachat-core-rust project.
:param target: The rust build target, `debug` or `release`.
"""
flags = types.SimpleNamespace()
flags = {}
if platform.system() == 'Darwin':
flags.libs = ['resolv', 'dl']
flags.extra_link_args = [
flags['libraries'] = ['resolv', 'dl']
flags['extra_link_args'] = [
'-framework', 'CoreFoundation',
'-framework', 'CoreServices',
'-framework', 'Security',
]
elif platform.system() == 'Linux':
flags.libs = ['rt', 'dl', 'm']
flags.extra_link_args = []
flags['libraries'] = ['rt', 'dl', 'm']
flags['extra_link_args'] = []
else:
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
target_dir = os.environ.get("CARGO_TARGET_DIR")
if target_dir is None:
target_dir = os.path.join(projdir, 'target')
flags.objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
assert os.path.exists(flags.objs[0]), flags.objs
flags.incs = [os.path.join(projdir, 'deltachat-ffi')]
flags['extra_objects'] = [os.path.join(target_dir, target, 'libdeltachat.a')]
assert os.path.exists(flags['extra_objects'][0]), flags['extra_objects']
flags['include_dirs'] = [os.path.join(projdir, 'deltachat-ffi')]
return flags
def system_build_flags():
"""Construct build flags for building against an installed libdeltachat."""
flags = types.SimpleNamespace()
flags.libs = ['deltachat']
flags.objs = []
flags.incs = []
flags.extra_link_args = []
return flags
return pkgconfig.parse('deltachat')
def extract_functions(flags):
@@ -69,7 +64,7 @@ def extract_functions(flags):
src_fp.write('#include <deltachat.h>')
cc.preprocess(source=src_name,
output_file=dst_name,
include_dirs=flags.incs,
include_dirs=flags['include_dirs'],
macros=[('PY_CFFI', '1')])
with open(dst_name, "r") as dst_fp:
return dst_fp.read()
@@ -105,7 +100,7 @@ def find_header(flags):
try:
os.chdir(tmpdir)
cc.compile(sources=["where.c"],
include_dirs=flags.incs,
include_dirs=flags['include_dirs'],
macros=[("PY_CFFI_INC", "1")])
finally:
os.chdir(cwd)
@@ -183,10 +178,7 @@ def ffibuilder():
return DC_EVENT_DATA2_IS_STRING(e);
}
""",
include_dirs=flags.incs,
libraries=flags.libs,
extra_objects=flags.objs,
extra_link_args=flags.extra_link_args,
**flags,
)
builder.cdef("""
typedef int... time_t;

View File

@@ -22,6 +22,29 @@ class MissingCredentials(ValueError):
""" Account is missing `addr` and `mail_pw` config values. """
def get_core_info():
""" get some system info. """
from tempfile import NamedTemporaryFile
with NamedTemporaryFile() as path:
path.close()
return get_dc_info_as_dict(ffi.gc(
lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL),
lib.dc_context_unref,
))
def get_dc_info_as_dict(dc_context):
lines = from_dc_charpointer(lib.dc_get_info(dc_context))
info_dict = {}
for line in lines.split("\n"):
if not line.strip():
continue
key, value = line.split("=", 1)
info_dict[key.lower()] = value
return info_dict
class Account(object):
""" Each account is tied to a sqlite database file which is fully managed
by the underlying deltachat core library. All public Account methods are
@@ -67,6 +90,9 @@ class Account(object):
""" re-enable logging. """
self._logging = True
def __repr__(self):
return "<Account path={}>".format(self.db_path)
# def __del__(self):
# self.shutdown()
@@ -81,14 +107,7 @@ class Account(object):
def get_info(self) -> Dict[str, str]:
""" return dictionary of built config parameters. """
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
d = {}
for line in lines.split("\n"):
if not line.strip():
continue
key, value = line.split("=", 1)
d[key.lower()] = value
return d
return get_dc_info_as_dict(self._dc_context)
def dump_account_info(self, logfile):
def log(*args, **kwargs):
@@ -129,6 +148,8 @@ class Account(object):
namebytes = name.encode("utf8")
if namebytes == b"addr" and self.is_configured():
raise ValueError("can not change 'addr' after account is configured.")
if isinstance(value, (int, bool)):
value = str(int(value))
if value is not None:
valuebytes = value.encode("utf8")
else:
@@ -169,7 +190,7 @@ class Account(object):
:returns: None
"""
for key, value in kwargs.items():
self.set_config(key, str(value))
self.set_config(key, value)
def is_configured(self) -> bool:
""" determine if the account is configured already; an initial connection
@@ -569,6 +590,8 @@ class Account(object):
""" add an account plugin which implements one or more of
the :class:`deltachat.hookspec.PerAccount` hooks.
"""
if name and self._pm.has_plugin(name=name):
self._pm.unregister(name=name)
self._pm.register(plugin, name=name)
self._pm.check_pending()
return plugin
@@ -672,28 +695,25 @@ class Account(object):
if self._dc_context is None:
return
self.stop_io()
self.log("remove dc_context references")
# if _dc_context is unref'ed the event thread should quickly
# receive the termination signal. However, some python code might
# still hold a reference and so we use a secondary signal
# to make sure the even thread terminates if it receives any new
# event, indepedently from waiting for the core to send NULL to
# get_next_event().
# mark the event thread for shutdown (latest on next incoming event)
self._event_thread.mark_shutdown()
self._dc_context = None
# stop_io also causes an info event which will wake up
# the EventThread's inner loop and let it notice the shutdown marker.
self.stop_io()
self.log("wait for event thread to finish")
try:
self._event_thread.wait(timeout=2)
self._event_thread.wait(timeout=5)
except RuntimeError as e:
self.log("Waiting for event thread failed: {}".format(e))
if self._event_thread.is_alive():
self.log("WARN: event thread did not terminate yet, ignoring.")
self.log("remove dc_context references, making the Account unusable")
self._dc_context = None
self._shutdown_event.set()
hook = hookspec.Global._get_plugin_manager().hook

View File

@@ -5,7 +5,7 @@ import calendar
import json
from datetime import datetime, timezone
import os
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array
from .capi import lib, ffi
from . import const
from .message import Message
@@ -64,7 +64,7 @@ class Chat(object):
def is_group(self) -> bool:
""" return true if this chat is a group chat.
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
:returns: True if chat is a group-chat, false otherwise
"""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
@@ -517,7 +517,7 @@ class Chat(object):
lib.dc_array_get_timestamp(dc_array, i),
timezone.utc
),
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
marker=from_optional_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
)
for i in range(lib.dc_array_get_cnt(dc_array))
]

View File

@@ -4,63 +4,20 @@ and for cleaning up inbox/mvbox for each test function run.
"""
import io
import email
import ssl
import pathlib
from imapclient import IMAPClient
from imapclient.exceptions import IMAPClientError
from contextlib import contextmanager
from imap_tools import MailBox, MailBoxTls, errors, AND, Header, MailMessageFlags, MailMessage
import imaplib
import deltachat
from deltachat import const, Account
from typing import List
SEEN = b'\\Seen'
DELETED = b'\\Deleted'
FLAGS = b'FLAGS'
FETCH = b'FETCH'
ALL = "1:*"
@deltachat.global_hookimpl
def dc_account_extra_configure(account):
""" Reset the account (we reuse accounts across tests)
and make 'account.direct_imap' available for direct IMAP ops.
"""
try:
if not hasattr(account, "direct_imap"):
imap = DirectImap(account)
for folder in imap.list_folders():
if folder.lower() == "inbox" or folder.lower() == "deltachat":
assert imap.select_folder(folder)
imap.delete(ALL, expunge=True)
else:
imap.conn.delete_folder(folder)
# We just deleted the folder, so we have to make DC forget about it, too
if account.get_config("configured_sentbox_folder") == folder:
account.set_config("configured_sentbox_folder", None)
if account.get_config("configured_spam_folder") == folder:
account.set_config("configured_spam_folder", None)
setattr(account, "direct_imap", imap)
except Exception as e:
# Uncaught exceptions here would lead to a timeout without any note written to the log
# 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
def dc_account_after_shutdown(account):
""" shutdown the imap connection if there is one. """
imap = getattr(account, "direct_imap", None)
if imap is not None:
imap.shutdown()
del account.direct_imap
class DirectImap:
def __init__(self, account: Account) -> None:
self.account = account
@@ -88,37 +45,30 @@ class DirectImap:
ssl_context.verify_mode = ssl.CERT_NONE
if security == const.DC_SOCKET_STARTTLS:
self.conn = IMAPClient(host, port, ssl=False)
self.conn.starttls(ssl_context)
elif security == const.DC_SOCKET_PLAIN:
self.conn = IMAPClient(host, port, ssl=False)
elif security == const.DC_SOCKET_SSL:
self.conn = IMAPClient(host, port, ssl_context=ssl_context)
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
elif security == const.DC_SOCKET_PLAIN or security == const.DC_SOCKET_SSL:
self.conn = MailBox(host, port, ssl_context=ssl_context)
self.conn.login(user, pw)
self.select_folder("INBOX")
def shutdown(self):
try:
self.conn.idle_done()
except (OSError, IMAPClientError):
pass
try:
self.conn.logout()
except (OSError, IMAPClientError):
except (OSError, imaplib.IMAP4.abort):
print("Could not logout direct_imap conn")
def create_folder(self, foldername):
try:
self.conn.create_folder(foldername)
except imaplib.IMAP4.error as e:
self.conn.folder.create(foldername)
except errors.MailboxFolderCreateError as e:
print("Can't create", foldername, "probably it already exists:", str(e))
def select_folder(self, foldername):
def select_folder(self, foldername: str) -> tuple:
assert not self._idling
return self.conn.select_folder(foldername)
return self.conn.folder.set(foldername)
def select_config_folder(self, config_name):
def select_config_folder(self, config_name: str):
""" Return info about selected folder if it is
configured, otherwise None. """
if "_" not in config_name:
@@ -127,50 +77,36 @@ class DirectImap:
if foldername:
return self.select_folder(foldername)
def list_folders(self):
def list_folders(self) -> List[str]:
""" return list of all existing folder names"""
assert not self._idling
folders = []
for meta, sep, foldername in self.conn.list_folders():
folders.append(foldername)
return folders
return [folder.name for folder in self.conn.folder.list()]
def delete(self, range, expunge=True):
def delete(self, uid_list: str, expunge=True):
""" delete a range of messages (imap-syntax).
If expunge is true, perform the expunge-operation
to make sure the messages are really gone and not
just flagged as deleted.
"""
self.conn.set_flags(range, [DELETED])
self.conn.client.uid('STORE', uid_list, '+FLAGS', r'(\Deleted)')
if expunge:
self.conn.expunge()
def get_all_messages(self):
def get_all_messages(self) -> List[MailMessage]:
assert not self._idling
return [mail for mail in self.conn.fetch()]
# Flush unsolicited responses. IMAPClient has problems
# dealing with them: https://github.com/mjs/imapclient/issues/334
# When this NOOP was introduced, next FETCH returned empty
# result instead of a single message, even though IMAP server
# can only return more untagged responses than required, not
# less.
self.conn.noop()
return self.conn.fetch(ALL, [FLAGS])
def get_unread_messages(self):
def get_unread_messages(self) -> List[str]:
assert not self._idling
res = self.conn.fetch(ALL, [FLAGS])
return [uid for uid in res
if SEEN not in res[uid][FLAGS]]
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
def mark_all_read(self):
messages = self.get_unread_messages()
if messages:
res = self.conn.set_flags(messages, [SEEN])
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
print("marked seen:", messages, res)
def get_unread_cnt(self):
def get_unread_cnt(self) -> int:
return len(self.get_unread_messages())
def dump_imap_structures(self, dir, logfile):
@@ -192,75 +128,84 @@ class DirectImap:
log("---------", imapfolder, len(messages), "messages ---------")
# get message content without auto-marking it as seen
# fetching 'RFC822' would mark it as seen.
requested = [b'BODY.PEEK[]', FLAGS]
for uid, data in self.conn.fetch(messages, requested).items():
body_bytes = data[b'BODY[]']
if not body_bytes:
log("Message", uid, "has empty body")
for msg in self.conn.fetch(mark_seen=False):
body = getattr(msg.obj, "text", None)
if not body:
body = getattr(msg.obj, "html", None)
if not body:
log("Message", msg.uid, "has empty body")
continue
flags = data[FLAGS]
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
path.mkdir(parents=True, exist_ok=True)
fn = path.joinpath(str(uid))
fn.write_bytes(body_bytes)
log("Message", uid, fn)
email_message = email.message_from_bytes(body_bytes)
log("Message", uid, flags, "Message-Id:", email_message.get("Message-Id"))
fn = path.joinpath(str(msg.uid))
fn.write_bytes(body)
log("Message", msg.uid, fn)
log("Message", msg.uid, msg.flags, "Message-Id:", msg.obj.get("Message-Id"))
if empty_folders:
log("--------- EMPTY FOLDERS:", empty_folders)
print(stream.getvalue(), file=logfile)
def idle_start(self):
""" switch this connection to idle mode. non-blocking. """
assert not self._idling
res = self.conn.idle()
self._idling = True
return res
@contextmanager
def idle(self):
""" return Idle ContextManager. """
idle_manager = IdleManager(self)
try:
yield idle_manager
finally:
idle_manager.done()
def idle_check(self, terminate=False):
""" (blocking) wait for next idle message from server. """
assert self._idling
self.account.log("imap-direct: calling idle_check")
res = self.conn.idle_check(timeout=30)
if len(res) == 0:
raise TimeoutError
if terminate:
self.idle_done()
self.account.log("imap-direct: idle_check returned {!r}".format(res))
return res
def idle_wait_for_seen(self):
""" Return first message with SEEN flag
from a running idle-stream REtiurn.
"""
while 1:
for item in self.idle_check():
if item[1] == FETCH:
if item[2][0] == FLAGS:
if SEEN in item[2][1]:
return item[0]
def idle_done(self):
""" send idle-done to server if we are currently in idle mode. """
if self._idling:
res = self.conn.idle_done()
self._idling = False
return res
def append(self, folder, msg):
def append(self, folder: str, msg: str):
"""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)
self.conn.append(bytes(msg, encoding='ascii'), folder)
def get_uid_by_message_id(self, message_id):
msgs = self.conn.search(['HEADER', 'MESSAGE-ID', message_id])
def get_uid_by_message_id(self, message_id) -> str:
msgs = [msg.uid for msg in self.conn.fetch(AND(header=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]
class IdleManager:
def __init__(self, direct_imap):
self.direct_imap = direct_imap
self.log = direct_imap.account.log
# fetch latest messages before starting idle so that it only
# returns messages that arrive anew
self.direct_imap.conn.fetch("1:*")
self.direct_imap.conn.idle.start()
def check(self, timeout=None) -> List[bytes]:
""" (blocking) wait for next idle message from server. """
self.log("imap-direct: calling idle_check")
res = self.direct_imap.conn.idle.poll(timeout=timeout)
self.log("imap-direct: idle_check returned {!r}".format(res))
return res
def wait_for_new_message(self, timeout=None) -> bytes:
while 1:
for item in self.check(timeout=timeout):
if b'EXISTS' in item or b'RECENT' in item:
return item
def wait_for_seen(self, timeout=None) -> int:
""" Return first message with SEEN flag from a running idle-stream.
"""
while 1:
for item in self.check(timeout=timeout):
if FETCH in item:
self.log(str(item))
if FLAGS in item and rb'\Seen' in item:
return int(item.split(b' ')[1])
def done(self):
""" send idle-done to server if we are currently in idle mode. """
res = self.direct_imap.conn.idle.stop()
return res

View File

@@ -1,5 +1,8 @@
import threading
import sys
import traceback
import time
import io
import re
import os
from queue import Queue, Empty
@@ -29,10 +32,12 @@ class FFIEventLogger:
# to prevent garbled logging
_loglock = threading.RLock()
def __init__(self, account) -> None:
def __init__(self, account, logid=None, init_time=None) -> None:
self.account = account
self.logid = self.account.get_config("displayname")
self.init_time = time.time()
self.logid = logid or self.account.get_config("displayname")
if init_time is None:
init_time = time.time()
self.init_time = init_time
@account_hookimpl
def ac_process_ffi_event(self, ffi_event: FFIEvent) -> None:
@@ -156,14 +161,14 @@ class FFIEventTracker:
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
break
def wait_all_initial_fetches(self):
def wait_idle_inbox_ready(self):
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
so that new messages are not mistaken for old ones:
- ac1 and ac2 are created
- ac1 sends a message to ac2
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
- therefore no DC_EVENT_INCOMING_MSG is sent"""
self.get_info_contains("Done fetching existing messages")
self.get_info_contains("INBOX: Idle entering")
def wait_next_incoming_message(self):
""" wait for and return next incoming message. """
@@ -225,9 +230,7 @@ class EventThread(threading.Thread):
)
while not self._marked_for_shutdown:
event = lib.dc_get_next_event(event_emitter)
if event == ffi.NULL:
break
if self._marked_for_shutdown:
if event == ffi.NULL or self._marked_for_shutdown:
break
evt = lib.dc_event_get_id(event)
data1 = lib.dc_event_get_data1_int(event)
@@ -241,15 +244,23 @@ class EventThread(threading.Thread):
lib.dc_event_unref(event)
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
try:
with self.swallow_and_log_exception("ac_process_ffi_event {}".format(ffi_event)):
self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event)
for name, kwargs in self._map_ffi_event(ffi_event):
self.account.log("calling hook name={} kwargs={}".format(name, kwargs))
hook = getattr(self.account._pm.hook, name)
for name, kwargs in self._map_ffi_event(ffi_event):
hook = getattr(self.account._pm.hook, name)
info = "call {} kwargs={} failed".format(name, kwargs)
with self.swallow_and_log_exception(info):
hook(**kwargs)
except Exception:
if self.account._dc_context is not None:
raise
@contextmanager
def swallow_and_log_exception(self, info):
try:
yield
except Exception as ex:
logfile = io.StringIO()
traceback.print_exception(*sys.exc_info(), file=logfile)
self.account.log("{}\nException {}\nTraceback:\n{}"
.format(info, ex, logfile.getvalue()))
def _map_ffi_event(self, ffi_event: FFIEvent):
name = ffi_event.name

View File

@@ -8,35 +8,43 @@ import threading
import fnmatch
import time
import weakref
import tempfile
from typing import List, Dict, Callable
from queue import Queue
from typing import List, Callable
import pytest
import requests
import pathlib
from . import Account, const
from .capi import lib
from . import Account, const, account_hookimpl, get_core_info
from .events import FFIEventLogger, FFIEventTracker
from _pytest._code import Source
from deltachat import direct_imap
import deltachat
def pytest_addoption(parser):
parser.addoption(
group = parser.getgroup("deltachat testplugin options")
group.addoption(
"--liveconfig", action="store", default=None,
help="a file with >=2 lines where each line "
"contains NAME=VALUE config settings for one account"
)
parser.addoption(
group.addoption(
"--ignored", action="store_true",
help="Also run tests marked with the ignored marker",
)
parser.addoption(
group.addoption(
"--strict-tls", action="store_true",
help="Never accept invalid TLS certificates for test accounts",
)
group.addoption(
"--extra-info", action="store_true",
help="show more info on failures (imap server state, config)"
)
group.addoption(
"--debug-setup", action="store_true",
help="show events during configure and start io phases of online accounts"
)
def pytest_configure(config):
@@ -91,9 +99,12 @@ def pytest_configure(config):
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item):
self.enable_logging(item)
logging = item.config.getoption("--extra-info")
if logging:
self.enable_logging(item)
yield
self.disable_logging(item)
if logging:
self.disable_logging(item)
la = LoggingAspect()
config.pluginmanager.register(la)
@@ -101,20 +112,12 @@ def pytest_configure(config):
def pytest_report_header(config, startdir):
summary = []
t = tempfile.mktemp()
try:
ac = Account(t)
info = ac.get_info()
ac.shutdown()
finally:
os.remove(t)
summary.extend(['Deltachat core={} sqlite={} journal_mode={}'.format(
info['deltachat_core_version'],
info['sqlite_version'],
info['journal_mode'],
)])
info = get_core_info()
summary = ['Deltachat core={} sqlite={} journal_mode={}'.format(
info['deltachat_core_version'],
info['sqlite_version'],
info['journal_mode'],
)]
cfg = config.option.liveconfig
if cfg:
@@ -126,57 +129,97 @@ def pytest_report_header(config, startdir):
return summary
class SessionLiveConfigFromFile:
def __init__(self, fn) -> None:
self.fn = fn
self.configlist = []
for line in open(fn):
if line.strip() and not line.strip().startswith('#'):
d = {}
for part in line.split():
name, value = part.split("=")
d[name] = value
self.configlist.append(d)
def get(self, index: int):
return self.configlist[index]
def exists(self) -> bool:
return bool(self.configlist)
class SessionLiveConfigFromURL:
configlist: List[Dict[str, str]]
def __init__(self, url: str) -> None:
self.configlist = []
self.url = url
def get(self, index: int):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url)
if res.status_code != 200:
pytest.skip("creating newtmpuser failed with code {}: '{}'".format(res.status_code, res.text))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config)
return config
def exists(self) -> bool:
return bool(self.configlist)
@pytest.fixture(scope="session")
def session_liveconfig(request):
liveconfig_opt = request.config.option.liveconfig
if liveconfig_opt:
if liveconfig_opt.startswith("http"):
return SessionLiveConfigFromURL(liveconfig_opt)
def testprocess(request):
return TestProcess(pytestconfig=request.config)
class TestProcess:
""" A pytest session-scoped instance to help with managing "live" account configurations.
"""
def __init__(self, pytestconfig):
self.pytestconfig = pytestconfig
self._addr2files = {}
self._configlist = []
def get_liveconfig_producer(self):
""" provide live account configs, cached on a per-test-process scope
so that test functions can re-use already known live configs.
Depending on the --liveconfig option this comes from
a HTTP provider or a file with a line specifying each accounts config.
"""
liveconfig_opt = self.pytestconfig.getoption("--liveconfig")
if not liveconfig_opt:
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig to provide live accounts")
if not liveconfig_opt.startswith("http"):
for line in open(liveconfig_opt):
if line.strip() and not line.strip().startswith('#'):
d = {}
for part in line.split():
name, value = part.split("=")
d[name] = value
self._configlist.append(d)
yield from iter(self._configlist)
else:
return SessionLiveConfigFromFile(liveconfig_opt)
MAX_LIVE_CREATED_ACCOUNTS = 10
for index in range(MAX_LIVE_CREATED_ACCOUNTS):
try:
yield self._configlist[index]
except IndexError:
res = requests.post(liveconfig_opt)
if res.status_code != 200:
pytest.fail("newtmpuser count={} code={}: '{}'".format(
index, res.status_code, res.text))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
print("newtmpuser {}: addr={}".format(index, config["addr"]))
self._configlist.append(config)
yield config
pytest.fail("more than {} live accounts requested.".format(MAX_LIVE_CREATED_ACCOUNTS))
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
db_target_path = pathlib.Path(db_target_path)
assert not db_target_path.exists()
try:
filescache = self._addr2files[cache_addr]
except KeyError:
print("CACHE FAIL for", cache_addr)
return False
else:
print("CACHE HIT for", cache_addr)
targetdir = db_target_path.parent
write_dict_to_dir(filescache, targetdir)
return True
def cache_maybe_store_configured_db_files(self, acc):
addr = acc.get_config("addr")
assert acc.is_configured()
# don't overwrite existing entries
if addr not in self._addr2files:
print("storing cache for", addr)
basedir = pathlib.Path(acc.get_blobdir()).parent
self._addr2files[addr] = create_dict_from_files_in_path(basedir)
return True
def create_dict_from_files_in_path(base):
cachedict = {}
for path in base.glob("**/*"):
if path.is_file():
cachedict[path.relative_to(base)] = path.read_bytes()
return cachedict
def write_dict_to_dir(dic, target_dir):
assert dic
for relpath, content in dic.items():
path = target_dir.joinpath(relpath)
if not path.parent.exists():
os.makedirs(path.parent)
path.write_bytes(content)
@pytest.fixture
@@ -209,257 +252,349 @@ def data(request):
return Data()
@pytest.fixture
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
class ACSetup:
""" accounts setup helper to deal with multiple configure-process
and io & imap initialization phases. From tests, use the higher level
public ACFactory methods instead of its private helper class.
"""
CONFIGURING = "CONFIGURING"
CONFIGURED = "CONFIGURED"
IDLEREADY = "IDLEREADY"
class AccountMaker:
_finalizers: List[Callable[[], None]]
_accounts: List[Account]
def __init__(self, testprocess, init_time):
self._configured_events = Queue()
self._account2state = {}
self._imap_cleaned = set()
self.testprocess = testprocess
self.init_time = init_time
def __init__(self) -> None:
self.live_count = 0
self.offline_count = 0
self._finalizers = []
self._accounts = []
self.init_time = time.time()
self._generated_keys = ["alice", "bob", "charlie",
def log(self, *args):
print("[acsetup]", "{:.3f}".format(time.time() - self.init_time), *args)
def add_configured(self, account):
""" add an already configured account. """
assert account.is_configured()
self._account2state[account] = self.CONFIGURED
self.log("added already configured account", account, account.get_config("addr"))
def start_configure(self, account, reconfigure=False):
""" add an account and start its configure process. """
class PendingTracker:
@account_hookimpl
def ac_configure_completed(this, success):
self._configured_events.put((account, success))
account.add_account_plugin(PendingTracker(), name="pending_tracker")
self._account2state[account] = self.CONFIGURING
account.configure(reconfigure=reconfigure)
self.log("started configure on", account)
def wait_one_configured(self, account):
""" wait until this account has successfully configured. """
if self._account2state[account] == self.CONFIGURING:
while 1:
acc = self._pop_config_success()
if acc == account:
break
self.init_imap(acc)
self.init_logging(acc)
acc._evtracker.consume_events()
def bring_online(self):
""" Wait for all accounts to become ready to receive messages.
This will initialize logging, start IO and the direct_imap attribute
for each account which either is CONFIGURED already or which is CONFIGURING
and successfully completing the configuration process.
"""
print("wait_all_configured finds accounts=", self._account2state)
for acc, state in self._account2state.items():
if state == self.CONFIGURED:
self._onconfigure_start_io(acc)
self._account2state[acc] = self.IDLEREADY
while self.CONFIGURING in self._account2state.values():
acc = self._pop_config_success()
self._onconfigure_start_io(acc)
self._account2state[acc] = self.IDLEREADY
print("finished, account2state", self._account2state)
def _pop_config_success(self):
acc, success = self._configured_events.get()
if not success:
pytest.fail("configuring online account failed: {}".format(acc))
self._account2state[acc] = self.CONFIGURED
return acc
def _onconfigure_start_io(self, acc):
self.init_imap(acc)
self.init_logging(acc)
acc.start_io()
print(acc._logid, "waiting for inbox IDLE to become ready")
acc._evtracker.wait_idle_inbox_ready()
acc._evtracker.consume_events()
acc.log("inbox IDLE ready")
def init_logging(self, acc):
""" idempotent function for initializing logging (will replace existing logger). """
logger = FFIEventLogger(acc, logid=acc._logid, init_time=self.init_time)
acc.add_account_plugin(logger, name="logger-" + acc._logid)
def init_imap(self, acc):
""" initialize direct_imap and cleanup server state. """
from deltachat.direct_imap import DirectImap
assert acc.is_configured()
if not hasattr(acc, "direct_imap"):
acc.direct_imap = DirectImap(acc)
addr = acc.get_config("addr")
if addr not in self._imap_cleaned:
imap = acc.direct_imap
for folder in imap.list_folders():
if folder.lower() == "inbox" or folder.lower() == "deltachat":
assert imap.select_folder(folder)
imap.delete("1:*", expunge=True)
else:
imap.conn.folder.delete(folder)
acc.log("imap cleaned for addr {}".format(addr))
self._imap_cleaned.add(addr)
class ACFactory:
_finalizers: List[Callable[[], None]]
_accounts: List[Account]
def __init__(self, request, testprocess, tmpdir, data) -> None:
self.init_time = time.time()
self.tmpdir = tmpdir
self.pytestconfig = request.config
self.data = data
self.testprocess = testprocess
self._liveconfig_producer = testprocess.get_liveconfig_producer()
self._finalizers = []
self._accounts = []
self._acsetup = ACSetup(testprocess, self.init_time)
self._preconfigured_keys = ["alice", "bob", "charlie",
"dom", "elena", "fiona"]
self.set_logging_default(False)
deltachat.register_global_plugin(direct_imap)
self.set_logging_default(False)
request.addfinalizer(self.finalize)
def finalize(self):
while self._finalizers:
fin = self._finalizers.pop()
fin()
def log(self, *args):
print("[acfactory]", "{:.3f}".format(time.time() - self.init_time), *args)
while self._accounts:
acc = self._accounts.pop()
def finalize(self):
while self._finalizers:
fin = self._finalizers.pop()
fin()
while self._accounts:
acc = self._accounts.pop()
if acc is not None:
imap = getattr(acc, "direct_imap", None)
if imap is not None:
imap.shutdown()
del acc.direct_imap
acc.shutdown()
acc.disable_logging()
deltachat.unregister_global_plugin(direct_imap)
def make_account(self, path, logid, quiet=False):
ac = Account(path, logging=self._logging)
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
ac._evtracker.set_timeout(30)
ac.addr = ac.get_self_contact().addr
ac.set_config("displayname", logid)
if not quiet:
logger = FFIEventLogger(ac)
logger.init_time = self.init_time
ac.add_account_plugin(logger)
self._accounts.append(ac)
return ac
def get_next_liveconfig(self):
""" Base function to get functional online configurations
where we can make valid SMTP and IMAP connections with.
"""
configdict = next(self._liveconfig_producer).copy()
if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1"
def set_logging_default(self, logging):
self._logging = bool(logging)
if self.pytestconfig.getoption("--strict-tls"):
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
def get_unconfigured_account(self):
self.offline_count += 1
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
return self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
assert "addr" in configdict and "mail_pw" in configdict
return configdict
def _preconfigure_key(self, account, addr):
# Only set a key if we haven't used it yet for another account.
if self._generated_keys:
keyname = self._generated_keys.pop(0)
fname_pub = data.read_path("key/{name}-public.asc".format(name=keyname))
fname_sec = data.read_path("key/{name}-secret.asc".format(name=keyname))
if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec)
return True
else:
print("WARN: could not use preconfigured keys for {!r}".format(addr))
def _get_cached_account(self, addr):
if addr in self.testprocess._addr2files:
return self._getaccount(addr)
def get_configured_offline_account(self):
ac = self.get_unconfigured_account()
def get_unconfigured_account(self):
return self._getaccount()
# do a pseudo-configured account
addr = "addr{}@offline.org".format(self.offline_count)
ac.set_config("addr", addr)
self._preconfigure_key(ac, addr)
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
ac.set_config("mail_pw", "123")
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
lib.dc_set_config(ac._dc_context, b"configured", b"1")
return ac
def _getaccount(self, try_cache_addr=None):
logid = "ac{}".format(len(self._accounts) + 1)
# we need to use fixed database basename for maybe_cache_* functions to work
path = self.tmpdir.mkdir(logid).join("dc.db")
if try_cache_addr:
self.testprocess.cache_maybe_retrieve_configured_db_files(try_cache_addr, path)
ac = Account(path.strpath, logging=self._logging)
ac._logid = logid # later instantiated FFIEventLogger needs this
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
if self.pytestconfig.getoption("--debug-setup"):
self._acsetup.init_logging(ac)
self._accounts.append(ac)
return ac
def get_online_config(self, pre_generated_key=True, quiet=False):
if not session_liveconfig:
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
configdict = session_liveconfig.get(self.live_count)
self.live_count += 1
if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1"
def set_logging_default(self, logging):
self._logging = bool(logging)
if pytestconfig.getoption("--strict-tls"):
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
def remove_preconfigured_keys(self):
self._preconfigured_keys = []
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count), quiet=quiet)
if pre_generated_key:
self._preconfigure_key(ac, configdict['addr'])
return ac, dict(configdict)
def _preconfigure_key(self, account, addr):
# Only set a preconfigured key if we haven't used it yet for another account.
try:
keyname = self._preconfigured_keys.pop(0)
except IndexError:
pass
else:
fname_pub = self.data.read_path("key/{name}-public.asc".format(name=keyname))
fname_sec = self.data.read_path("key/{name}-secret.asc".format(name=keyname))
if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec)
return True
else:
print("WARN: could not use preconfigured keys for {!r}".format(addr))
def get_online_configuring_account(self, sentbox=False, move=False,
pre_generated_key=True, quiet=False, config={}):
ac, configdict = self.get_online_config(
pre_generated_key=pre_generated_key, quiet=quiet)
configdict.update(config)
configdict["mvbox_move"] = str(int(move))
configdict["sentbox_watch"] = str(int(sentbox))
ac.update_config(configdict)
ac._configtracker = ac.configure()
return ac
def get_pseudo_configured_account(self):
# do a pseudo-configured account
ac = self.get_unconfigured_account()
acname = ac._logid
addr = "{}@offline.org".format(acname)
ac.update_config(dict(
addr=addr, displayname=acname, mail_pw="123",
configured_addr=addr, configured_mail_pw="123",
configured="1",
))
self._preconfigure_key(ac, addr)
self._acsetup.init_logging(ac)
return ac
def get_one_online_account(self, pre_generated_key=True, move=False):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key, move=move)
self.wait_configure_and_start_io([ac1])
return ac1
def get_two_online_accounts(self, move=False, quiet=False):
ac1 = self.get_online_configuring_account(move=move, quiet=quiet)
ac2 = self.get_online_configuring_account(quiet=quiet)
self.wait_configure_and_start_io([ac1, ac2])
return ac1, ac2
def get_many_online_accounts(self, num, move=True):
accounts = [self.get_online_configuring_account(move=move, quiet=True)
for i in range(num)]
self.wait_configure_and_start_io(accounts)
for acc in accounts:
acc.add_account_plugin(FFIEventLogger(acc))
return accounts
def clone_online_account(self, account, pre_generated_key=True):
""" Clones addr, mail_pw, mvbox_move, sentbox_watch and the
direct_imap object of an online account. This simulates the user setting
up a new device without importing a backup.
`pre_generated_key` only means that a key from python/tests/data/key is
used in order to speed things up.
"""
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, account.get_config("addr"))
ac.update_config(dict(
addr=account.get_config("addr"),
mail_pw=account.get_config("mail_pw"),
mvbox_move=account.get_config("mvbox_move"),
sentbox_watch=account.get_config("sentbox_watch"),
))
if hasattr(account, "direct_imap"):
# Attach the existing direct_imap. If we did not do this, a new one would be created and
# delete existing messages (see dc_account_extra_configure(configure))
ac.direct_imap = account.direct_imap
ac._configtracker = ac.configure()
return ac
def wait_configure_and_start_io(self, accounts=None):
if accounts is None:
accounts = self._accounts[:]
started_accounts = []
for acc in accounts:
if acc not in started_accounts:
self.wait_configure(acc)
acc.set_config("bcc_self", "0")
if acc.is_configured():
acc.start_io()
started_accounts.append(acc)
print("{}: {} account was started".format(
acc.get_config("displayname"), acc.get_config("addr")))
for acc in started_accounts:
acc._evtracker.wait_all_initial_fetches()
def wait_configure(self, acc):
if hasattr(acc, "_configtracker"):
acc._configtracker.wait_finish()
acc._evtracker.consume_events()
acc.get_device_chat().mark_noticed()
del acc._configtracker
def run_bot_process(self, module, ffi=True):
fn = module.__file__
bot_ac, bot_cfg = self.get_online_config()
# Avoid starting ac so we don't interfere with the bot operating on
# the same database.
self._accounts.remove(bot_ac)
args = [
sys.executable,
"-u",
fn,
"--email", bot_cfg["addr"],
"--password", bot_cfg["mail_pw"],
bot_ac.db_path,
]
if ffi:
args.insert(-1, "--show-ffi")
print("$", " ".join(args))
popen = subprocess.Popen(
args=args,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
bufsize=0, # line buffering
close_fds=True, # close all FDs other than 0/1/2
universal_newlines=True # give back text
def new_online_configuring_account(self, cloned_from=None, cache=False, **kwargs):
if cloned_from is None:
configdict = self.get_next_liveconfig()
else:
# XXX we might want to transfer the key to the new account
configdict = dict(
addr=cloned_from.get_config("addr"),
mail_pw=cloned_from.get_config("mail_pw"),
)
bot = BotProcess(popen, bot_cfg)
self._finalizers.append(bot.kill)
return bot
configdict.update(kwargs)
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
if ac is not None:
# make sure we consume a preconfig key, as if we had created a fresh account
self._preconfigured_keys.pop(0)
self._acsetup.add_configured(ac)
return ac
ac = self.prepare_account_from_liveconfig(configdict)
self._acsetup.start_configure(ac)
return ac
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_imap_structures(tmpdir, logfile=logfile)
def prepare_account_from_liveconfig(self, configdict):
ac = self.get_unconfigured_account()
assert "addr" in configdict and "mail_pw" in configdict, configdict
configdict.setdefault("bcc_self", False)
configdict.setdefault("mvbox_move", False)
configdict.setdefault("sentbox_watch", False)
ac.update_config(configdict)
self._preconfigure_key(ac, configdict["addr"])
return ac
def get_accepted_chat(self, ac1: Account, ac2: Account):
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
def wait_configured(self, account):
""" Wait until the specified account has successfully completed configure. """
self._acsetup.wait_one_configured(account)
def introduce_each_other(self, accounts, sending=True):
to_wait = []
for i, acc in enumerate(accounts):
for acc2 in accounts[i + 1:]:
chat = self.get_accepted_chat(acc, acc2)
if sending:
chat.send_text("hi")
to_wait.append(acc2)
acc2.create_chat(acc).send_text("hi back")
to_wait.append(acc)
for acc in to_wait:
acc._evtracker.wait_next_incoming_message()
def bring_accounts_online(self):
print("bringing accounts online")
self._acsetup.bring_online()
print("all accounts online")
am = AccountMaker()
request.addfinalizer(am.finalize)
def get_online_accounts(self, num):
accounts = [self.new_online_configuring_account(cache=True) for i in range(num)]
self.bring_accounts_online()
# we cache fully configured and started accounts
for acc in accounts:
self.testprocess.cache_maybe_store_configured_db_files(acc)
return accounts
def run_bot_process(self, module, ffi=True):
fn = module.__file__
bot_cfg = self.get_next_liveconfig()
bot_ac = self.prepare_account_from_liveconfig(bot_cfg)
# Forget ac as it will be opened by the bot subprocess
# but keep something in the list to not confuse account generation
self._accounts[self._accounts.index(bot_ac)] = None
args = [
sys.executable,
"-u",
fn,
"--email", bot_cfg["addr"],
"--password", bot_cfg["mail_pw"],
bot_ac.db_path,
]
if ffi:
args.insert(-1, "--show-ffi")
print("$", " ".join(args))
popen = subprocess.Popen(
args=args,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
bufsize=0, # line buffering
close_fds=True, # close all FDs other than 0/1/2
universal_newlines=True # give back text
)
bot = BotProcess(popen, addr=bot_cfg["addr"])
self._finalizers.append(bot.kill)
return bot
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:
imap.dump_imap_structures(self.tmpdir, logfile=logfile)
def get_accepted_chat(self, ac1: Account, ac2: Account):
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
def introduce_each_other(self, accounts, sending=True):
to_wait = []
for i, acc in enumerate(accounts):
for acc2 in accounts[i + 1:]:
chat = self.get_accepted_chat(acc, acc2)
if sending:
chat.send_text("hi")
to_wait.append(acc2)
acc2.create_chat(acc).send_text("hi back")
to_wait.append(acc)
for acc in to_wait:
acc.log("waiting for incoming message")
acc._evtracker.wait_next_incoming_message()
@pytest.fixture
def acfactory(request, tmpdir, testprocess, data):
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
yield am
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
logfile = io.StringIO()
am.dump_imap_summary(logfile=logfile)
print(logfile.getvalue())
# request.node.add_report_section("call", "imap-server-state", s)
if testprocess.pytestconfig.getoption("--extra-info"):
logfile = io.StringIO()
am.dump_imap_summary(logfile=logfile)
print(logfile.getvalue())
# request.node.add_report_section("call", "imap-server-state", s)
class BotProcess:
stdout_queue: queue.Queue
def __init__(self, popen, bot_cfg) -> None:
def __init__(self, popen, addr) -> None:
self.popen = popen
self.addr = bot_cfg["addr"]
self.addr = addr
# we read stdout as quickly as we can in a thread and make
# the (unicode) lines available for readers through a queue.
@@ -483,7 +618,7 @@ class BotProcess:
def kill(self) -> None:
self.popen.kill()
def wait(self, timeout=30) -> None:
def wait(self, timeout=None) -> None:
self.popen.wait(timeout=timeout)
def fnmatch_lines(self, pattern_lines):
@@ -492,7 +627,7 @@ class BotProcess:
print("+++FNMATCH:", next_pattern)
ignored = []
while 1:
line = self.stdout_queue.get(timeout=15)
line = self.stdout_queue.get()
if line is None:
if ignored:
print("BOT stdout terminated after these lines")

View File

@@ -0,0 +1,20 @@
"""
run with:
pytest -vv --durations=10 bench_empty.py
to see timings of test setups.
"""
import pytest
BENCH_NUM = 3
class TestEmpty:
def test_prepare_setup_measurings(self, acfactory):
acfactory.get_online_accounts(BENCH_NUM)
@pytest.mark.parametrize("num", range(0, BENCH_NUM + 1))
def test_setup_online_accounts(self, acfactory, num):
acfactory.get_online_accounts(num)

View File

@@ -17,7 +17,7 @@ def test_db_busy_error(acfactory, tmpdir):
print("%3.2f %s" % (time.time() - starttime, string))
# make a number of accounts
accounts = acfactory.get_many_online_accounts(3, quiet=True)
accounts = acfactory.get_many_online_accounts(3)
log("created %s accounts" % len(accounts))
# put a bigfile into each account

View File

@@ -0,0 +1,481 @@
import sys
import pytest
class TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp):
accounts = acfactory.get_online_accounts(5)
acfactory.introduce_each_other(accounts)
ac1, ac5 = accounts.pop(), accounts.pop()
lp.sec("ac1: creating group chat with 3 other members")
chat = ac1.create_group_chat("title1", contacts=accounts)
lp.sec("ac1: send message to new group chat")
msg1 = chat.send_text("hello")
assert msg1.is_encrypted()
gossiped_timestamp = chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp > 0
assert chat.num_contacts() == 3 + 1
lp.sec("ac2: checking that the chat arrived correctly")
ac2 = accounts[0]
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "hello"
print("chat is", msg2.chat)
assert msg2.chat.num_contacts() == 4
lp.sec("ac3: checking that 'ac4' is a known contact")
ac3 = accounts[1]
msg3 = ac3._evtracker.wait_next_incoming_message()
assert msg3.text == "hello"
ac3_contacts = ac3.get_contacts()
assert len(ac3_contacts) == 4
ac4_contacts = ac3.get_contacts(query=accounts[2].get_config("addr"))
assert len(ac4_contacts) == 1
lp.sec("ac2: removing one contact")
to_remove = ac2.create_contact(accounts[-1])
msg2.chat.remove_contact(to_remove)
lp.sec("ac1: receiving system message about contact removal")
sysmsg = ac1._evtracker.wait_next_incoming_message()
assert to_remove.addr in sysmsg.text
assert sysmsg.chat.num_contacts() == 3
# Receiving message about removed contact does not reset gossip
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
lp.sec("ac1: sending another message to the chat")
chat.send_text("hello2")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello2"
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
lp.sec("ac1: adding fifth member to the chat")
chat.add_contact(ac5)
# Adding contact to chat resets gossiped_timestamp
assert chat.get_summary()["gossiped_timestamp"] >= gossiped_timestamp
lp.sec("ac2: receiving system message about contact addition")
sysmsg = ac2._evtracker.wait_next_incoming_message()
assert ac5.get_config("configured_addr") in sysmsg.text
assert sysmsg.chat.num_contacts() == 4
lp.sec("ac5: waiting for message about addition to the chat")
sysmsg = ac5._evtracker.wait_next_incoming_message()
msg = sysmsg.chat.send_text("hello!")
# Message should be encrypted because keys of other members are gossiped
assert msg.is_encrypted()
def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp):
"""
Test that user recreates group member list when it joins the group again.
ac1 creates a group with two other accounts: ac2 and ac3
Then it removes ac2, removes ac3 and adds ac2 back.
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
"""
lp.sec("setting up accounts, accepted with each other")
accounts = acfactory.get_online_accounts(3)
acfactory.introduce_each_other(accounts)
ac1, ac2, ac3 = accounts
lp.sec("ac1: creating group chat with 2 other members")
chat = ac1.create_group_chat("title1", contacts=[ac2, ac3])
assert not chat.is_promoted()
lp.sec("ac1: send message to new group chat")
msg = chat.send_text("hello")
assert chat.is_promoted() and msg.is_encrypted()
assert chat.num_contacts() == 3
lp.sec("checking that the chat arrived correctly")
for ac in accounts[1:]:
msg = ac._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
print("chat is", msg.chat)
assert msg.chat.num_contacts() == 3
lp.sec("ac1: removing ac2")
chat.remove_contact(ac2)
lp.sec("ac2: wait for a message about removal from the chat")
msg = ac2._evtracker.wait_next_incoming_message()
lp.sec("ac1: removing ac3")
chat.remove_contact(ac3)
lp.sec("ac1: adding ac2 back")
# Group is promoted, message is sent automatically
assert chat.is_promoted()
chat.add_contact(ac2)
lp.sec("ac2: check that ac3 is removed")
msg = ac2._evtracker.wait_next_incoming_message()
assert chat.num_contacts() == 2
assert msg.chat.num_contacts() == 2
acfactory.dump_imap_summary(sys.stdout)
def test_qr_verified_group_and_chatting(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_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()
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
assert chat2.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000)
lp.sec("ac2: read member added message")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert "added" in msg.text.lower()
lp.sec("ac1: send message")
msg_out = chat1.send_text("hello")
assert msg_out.is_encrypted()
lp.sec("ac2: read message and check it's verified chat")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert msg.chat.is_protected()
assert msg.is_encrypted()
lp.sec("ac2: send message and let ac1 read it")
chat2.send_text("world")
msg = ac1._evtracker.wait_next_incoming_message()
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()
@pytest.mark.parametrize("mvbox_move", [False, True])
def test_fetch_existing(acfactory, lp, mvbox_move):
"""Delta Chat reads the recipients from old emails sent by the user and adds them as contacts.
This way, we can already offer them some email addresses they can write to.
Also, the newest existing emails from each folder are fetched during onboarding.
Additionally tests that bcc_self messages moved to the mvbox/sentbox are marked as read."""
def assert_folders_configured(ac):
"""There was a bug that scan_folders() set the configured folders to None under some circumstances.
So, check that they are still configured:"""
assert ac.get_config("configured_sentbox_folder") == "Sent"
if mvbox_move:
assert ac.get_config("configured_mvbox_folder")
ac1 = acfactory.new_online_configuring_account(mvbox_move=mvbox_move)
ac2 = acfactory.new_online_configuring_account()
acfactory.wait_configured(ac1)
ac1.direct_imap.create_folder("Sent")
ac1.set_config("sentbox_watch", "1")
# We need to reconfigure to find the new "Sent" folder.
# `scan_folders()`, which runs automatically shortly after `start_io()` is invoked,
# would also find the "Sent" folder, but it would be too late:
# The sentbox thread, started by `start_io()`, would have seen that there is no
# ConfiguredSentboxFolder and do nothing.
acfactory._acsetup.start_configure(ac1, reconfigure=True)
acfactory.bring_accounts_online()
assert_folders_configured(ac1)
assert ac1.direct_imap.select_config_folder("mvbox" if mvbox_move else "inbox")
with ac1.direct_imap.idle() as idle1:
lp.sec("send out message with bcc to ourselves")
ac1.set_config("bcc_self", "1")
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message text")
assert_folders_configured(ac1)
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
assert idle1.wait_for_seen()
assert_folders_configured(ac1)
lp.sec("create a cloned ac1 and fetch contact history during configure")
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
ac1_clone.set_config("fetch_existing_msgs", "1")
acfactory.wait_configured(ac1_clone)
ac1_clone.start_io()
assert_folders_configured(ac1_clone)
lp.sec("check that ac2 contact was fetchted during configure")
ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
ac2_addr = ac2.get_config("addr")
assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())
assert_folders_configured(ac1_clone)
lp.sec("check that messages changed events arrive for the correct message")
msg = ac1_clone._evtracker.wait_next_messages_changed()
assert msg.text == "message text"
assert_folders_configured(ac1)
assert_folders_configured(ac1_clone)
def test_fetch_existing_msgs_group_and_single(acfactory, lp):
"""There was a bug concerning fetch-existing-msgs:
A sent a message to you, adding you to a group. This created a contact request.
You wrote a message to A, creating a chat.
...but the group stayed blocked.
So, after fetch-existing-msgs you have one contact request and one chat with the same person.
See https://github.com/deltachat/deltachat-core-rust/issues/2097"""
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account()
acfactory.bring_accounts_online()
lp.sec("receive a message")
ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message")
ac1._evtracker.wait_next_incoming_message()
lp.sec("send out message with bcc to ourselves")
with ac1.direct_imap.idle() as idle1:
ac1.set_config("bcc_self", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
# wait until the bcc_self message arrives
assert idle1.wait_for_seen()
lp.sec("Clone online account and let it fetch the existing messages")
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
ac1_clone.set_config("fetch_existing_msgs", "1")
acfactory.wait_configured(ac1_clone)
ac1_clone.start_io()
ac1_clone._evtracker.wait_idle_inbox_ready()
chats = ac1_clone.get_chats()
assert len(chats) == 4 # two newly created chats + self-chat + device-chat
group_chat = [c for c in chats if c.get_name() == "group name"][0]
assert group_chat.is_group()
private_chat, = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()]
assert not private_chat.is_group()
group_messages = group_chat.get_messages()
assert len(group_messages) == 1
assert group_messages[0].text == "incoming, unencrypted group message"
private_messages = private_chat.get_messages()
# We can't decrypt the message in this chat, so the chat is empty:
assert len(private_messages) == 0
def test_undecipherable_group(acfactory, lp):
"""Test how group messages that cannot be decrypted are
handled.
Group name is encrypted and plaintext subject is set to "..." in
this case, so we should assign the messages to existing chat
instead of creating a new one. Since there is no existing group
chat, the messages should be assigned to 1-1 chat with the sender
of the message.
"""
lp.sec("creating and configuring three accounts")
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
acfactory.introduce_each_other([ac1, ac2, ac3])
lp.sec("ac3 reinstalls DC and generates a new key")
ac3.stop_io()
acfactory.remove_preconfigured_keys()
ac4 = acfactory.new_online_configuring_account(cloned_from=ac3)
acfactory.wait_configured(ac4)
# Create contacts to make sure incoming messages are not treated as contact requests
chat41 = ac4.create_chat(ac1)
chat42 = ac4.create_chat(ac2)
ac4.start_io()
ac4._evtracker.wait_idle_inbox_ready()
lp.sec("ac1: creating group chat with 2 other members")
chat = ac1.create_group_chat("title", contacts=[ac2, ac3])
lp.sec("ac1: send message to new group chat")
msg = chat.send_text("hello")
lp.sec("ac2: checking that the chat arrived correctly")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert msg.is_encrypted(), "Message is not encrypted"
# ac4 cannot decrypt the message.
# Error message should be assigned to the chat with ac1.
lp.sec("ac4: checking that message is assigned to the sender chat")
error_msg = ac4._evtracker.wait_next_incoming_message()
assert error_msg.error # There is an error decrypting the message
assert error_msg.chat == chat41
lp.sec("ac2: sending a reply to the chat")
msg.chat.send_text("reply")
reply = ac1._evtracker.wait_next_incoming_message()
assert reply.text == "reply"
assert reply.is_encrypted(), "Reply is not encrypted"
lp.sec("ac4: checking that reply is assigned to ac2 chat")
error_reply = ac4._evtracker.wait_next_incoming_message()
assert error_reply.error # There is an error decrypting the message
assert error_reply.chat == chat42
# Test that ac4 replies to error messages don't appear in the
# group chat on ac1 and ac2.
lp.sec("ac4: replying to ac1 and ac2")
# Otherwise reply becomes a contact request.
chat41.send_text("I can't decrypt your message, ac1!")
chat42.send_text("I can't decrypt your message, ac2!")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.error is None
assert msg.text == "I can't decrypt your message, ac1!"
assert msg.is_encrypted(), "Message is not encrypted"
assert msg.chat == ac1.create_chat(ac3)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.error is None
assert msg.text == "I can't decrypt your message, ac2!"
assert msg.is_encrypted(), "Message is not encrypted"
assert msg.chat == ac2.create_chat(ac4)
def test_ephemeral_timer(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
lp.sec("ac1: create chat with ac2")
chat1 = ac1.create_chat(ac2)
chat2 = ac2.create_chat(ac1)
lp.sec("ac1: set ephemeral timer to 60")
chat1.set_ephemeral_timer(60)
lp.sec("ac1: check that ephemeral timer is set for chat")
assert chat1.get_ephemeral_timer() == 60
chat1_summary = chat1.get_summary()
assert chat1_summary["ephemeral_timer"] == {'Enabled': {'duration': 60}}
lp.sec("ac2: receive system message about ephemeral timer modification")
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
system_message1 = ac2._evtracker.wait_next_incoming_message()
assert chat2.get_ephemeral_timer() == 60
assert system_message1.is_system_message()
# Disabled until markers are implemented
# assert "Ephemeral timer: 60\n" in system_message1.get_message_info()
lp.sec("ac2: send message to ac1")
sent_message = chat2.send_text("message")
assert sent_message.ephemeral_timer == 60
assert "Ephemeral timer: 60\n" in sent_message.get_message_info()
# Timer is started immediately for sent messages
assert sent_message.ephemeral_timestamp is not None
assert "Expires: " in sent_message.get_message_info()
lp.sec("ac1: waiting for message from ac2")
text_message = ac1._evtracker.wait_next_incoming_message()
assert text_message.text == "message"
assert text_message.ephemeral_timer == 60
assert "Ephemeral timer: 60\n" in text_message.get_message_info()
# Timer should not start until message is displayed
assert text_message.ephemeral_timestamp is None
assert "Expires: " not in text_message.get_message_info()
text_message.mark_seen()
text_message = ac1.get_message_by_id(text_message.id)
assert text_message.ephemeral_timestamp is not None
assert "Expires: " in text_message.get_message_info()
lp.sec("ac2: set ephemeral timer to 0")
chat2.set_ephemeral_timer(0)
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
lp.sec("ac1: receive system message about ephemeral timer modification")
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
system_message2 = ac1._evtracker.wait_next_incoming_message()
assert system_message2.ephemeral_timer is None
assert "Ephemeral timer: " not in system_message2.get_message_info()
assert chat1.get_ephemeral_timer() == 0
def test_multidevice_sync_seen(acfactory, lp):
"""Test that message marked as seen on one device is marked as seen on another."""
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account()
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
acfactory.bring_accounts_online()
ac1.set_config("bcc_self", "1")
ac1_clone.set_config("bcc_self", "1")
ac1_chat = ac1.create_chat(ac2)
ac1_clone_chat = ac1_clone.create_chat(ac2)
ac2_chat = ac2.create_chat(ac1)
lp.sec("Send a message from ac2 to ac1 and check that it's 'fresh'")
ac2_chat.send_text("Hi")
ac1_message = ac1._evtracker.wait_next_incoming_message()
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
assert ac1_chat.count_fresh_messages() == 1
assert ac1_clone_chat.count_fresh_messages() == 1
assert ac1_message.is_in_fresh
assert ac1_clone_message.is_in_fresh
lp.sec("ac1 marks message as seen on the first device")
ac1.mark_seen_messages([ac1_message])
assert ac1_message.is_in_seen
lp.sec("ac1 clone detects that message is marked as seen")
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert ev.data1 == ac1_clone_chat.id
assert ac1_clone_message.is_in_seen
lp.sec("Send an ephemeral message from ac2 to ac1")
ac2_chat.set_ephemeral_timer(60)
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
ac1._evtracker.wait_next_incoming_message()
ac1_clone._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
ac1_clone._evtracker.wait_next_incoming_message()
ac2_chat.send_text("Foobar")
ac1_message = ac1._evtracker.wait_next_incoming_message()
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
assert "Expires: " not in ac1_clone_message.get_message_info()
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
assert "Expires: " not in ac1_clone_message.get_message_info()
ac1.mark_seen_messages([ac1_message])
assert ac1_message.is_in_seen
assert "Expires: " in ac1_message.get_message_info()
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert ev.data1 == ac1_clone_chat.id
assert ac1_clone_message.is_in_seen
# Test that the timer is started on the second device after synchronizing the seen status.
assert "Expires: " in ac1_clone_message.get_message_info()

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ def wait_msgs_changed(account, msgs_list):
class TestOnlineInCreation:
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
lp.sec("Creating in-creation file outside of blobdir")
@@ -43,7 +43,7 @@ class TestOnlineInCreation:
chat.prepare_message_file(src.strpath)
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
lp.sec("Creating file outside of blobdir")
@@ -56,7 +56,7 @@ class TestOnlineInCreation:
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
def test_forward_increation(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
wait_msgs_changed(ac1, [(0, 0)]) # why no chat id?

View File

@@ -0,0 +1,634 @@
from __future__ import print_function
import pytest
import os
import time
from deltachat import const, Account
from deltachat.message import Message
from deltachat.hookspec import account_hookimpl
from deltachat.capi import ffi, lib
from deltachat.cutil import iter_array
from datetime import datetime, timedelta, timezone
@pytest.mark.parametrize("msgtext,res", [
("Member Me (tmp1@x.org) removed by tmp2@x.org.",
("removed", "tmp1@x.org", "tmp2@x.org")),
("Member With space (tmp1@x.org) removed by tmp2@x.org.",
("removed", "tmp1@x.org", "tmp2@x.org")),
("Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
("removed", "tmp1@x.org", "tmp2@x.org")),
("Member With space (tmp1@x.org) removed by me",
("removed", "tmp1@x.org", "me")),
("Group left by some one (tmp1@x.org).",
("removed", "tmp1@x.org", "tmp1@x.org")),
("Group left by tmp1@x.org.",
("removed", "tmp1@x.org", "tmp1@x.org")),
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org", "tmp2@x.org")),
("Member nothing bla bla", None),
("Another unknown system message", None),
])
def test_parse_system_add_remove(msgtext, res):
from deltachat.message import parse_system_add_remove
out = parse_system_add_remove(msgtext)
assert out == res
class TestOfflineAccountBasic:
def test_wrong_db(self, tmpdir):
p = tmpdir.join("hello.db")
p.write("123")
account = Account(p.strpath)
assert not account.is_open()
def test_os_name(self, tmpdir):
p = tmpdir.join("hello.db")
# we can't easily test if os_name is used in X-Mailer
# outgoing messages without a full Online test
# but we at least check Account accepts the arg
ac1 = Account(p.strpath, os_name="solarpunk")
ac1.get_info()
def test_preconfigure_keypair(self, acfactory, data):
ac = acfactory.get_unconfigured_account()
alice_public = data.read_path("key/alice-public.asc")
alice_secret = data.read_path("key/alice-secret.asc")
assert alice_public and alice_secret
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
d = ac1.get_info()
assert d["arch"]
assert d["number_of_chats"] == "0"
assert d["bcc_self"] == "0"
def test_is_not_configured(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
assert not ac1.is_configured()
with pytest.raises(ValueError):
ac1.check_is_configured()
def test_wrong_config_keys(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
with pytest.raises(KeyError):
ac1.set_config("lqkwje", "value")
with pytest.raises(KeyError):
ac1.get_config("lqkwje")
def test_set_config_int_conversion(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
ac1.set_config("mvbox_move", False)
assert ac1.get_config("mvbox_move") == "0"
ac1.set_config("mvbox_move", True)
assert ac1.get_config("mvbox_move") == "1"
ac1.set_config("mvbox_move", 0)
assert ac1.get_config("mvbox_move") == "0"
ac1.set_config("mvbox_move", 1)
assert ac1.get_config("mvbox_move") == "1"
def test_update_config(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
ac1.update_config(dict(mvbox_move=False))
assert ac1.get_config("mvbox_move") == "0"
def test_has_savemime(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
assert "save_mime_headers" in ac1.get_config("sys.config_keys").split()
def test_has_bccself(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
assert ac1.get_config("bcc_self") == "0"
def test_selfcontact_if_unconfigured(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
assert not ac1.get_self_contact().addr
def test_selfcontact_configured(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
me = ac1.get_self_contact()
assert me.display_name
assert me.addr
def test_get_config_fails(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
with pytest.raises(KeyError):
ac1.get_config("123123")
def test_empty_group_bcc_self_enabled(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac1.set_config("bcc_self", "1")
chat = ac1.create_group_chat(name="group1")
msg = chat.send_text("msg1")
assert msg in chat.get_messages()
def test_empty_group_bcc_self_disabled(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac1.set_config("bcc_self", "0")
chat = ac1.create_group_chat(name="group1")
msg = chat.send_text("msg1")
assert msg in chat.get_messages()
class TestOfflineContact:
def test_contact_attr(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
contact2 = ac1.create_contact("some1@example.org", name="some1")
str(contact1)
repr(contact1)
assert contact1 == contact2
assert contact1.id
assert contact1.addr == "some1@example.org"
assert contact1.display_name == "some1"
assert not contact1.is_blocked()
assert not contact1.is_verified()
def test_get_blocked(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
contact2 = ac1.create_contact("some2@example.org", name="some2")
ac1.create_contact("some3@example.org", name="some3")
assert ac1.get_blocked_contacts() == []
contact1.block()
assert ac1.get_blocked_contacts() == [contact1]
contact2.block()
blocked = ac1.get_blocked_contacts()
assert len(blocked) == 2 and contact1 in blocked and contact2 in blocked
contact2.unblock()
assert ac1.get_blocked_contacts() == [contact1]
def test_create_self_contact(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact(ac1.get_config("addr"))
assert contact1.id == 1
def test_get_contacts_and_delete(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
contacts = ac1.get_contacts()
assert len(contacts) == 1
assert contact1 in contacts
assert not ac1.get_contacts(query="some2")
assert ac1.get_contacts(query="some1")
assert not ac1.get_contacts(only_verified=True)
assert len(ac1.get_contacts(with_self=True)) == 2
assert ac1.delete_contact(contact1)
assert contact1 not in ac1.get_contacts()
def test_get_contacts_and_delete_fails(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.com", name="some1")
msg = contact1.create_chat().send_text("one message")
assert not ac1.delete_contact(contact1)
assert not msg.filemime
def test_create_chat_flexibility(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
chat1 = ac1.create_chat(ac2)
chat2 = ac1.create_chat(ac2.get_self_contact().addr)
assert chat1 == chat2
ac3 = acfactory.get_unconfigured_account()
with pytest.raises(ValueError):
ac1.create_chat(ac3)
def test_contact_rename(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact = ac1.create_contact("some1@example.com", name="some1")
chat = ac1.create_chat(contact)
assert chat.get_name() == "some1"
ac1.create_contact("some1@example.com", name="renamed")
ev = ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
assert ev.data1 == chat.id
assert chat.get_name() == "renamed"
class TestOfflineChat:
@pytest.fixture
def ac1(self, acfactory):
return acfactory.get_pseudo_configured_account()
@pytest.fixture
def chat1(self, ac1):
return ac1.create_contact("some1@example.org", name="some1").create_chat()
def test_display(self, chat1):
str(chat1)
repr(chat1)
def test_is_group(self, chat1):
assert not chat1.is_group()
def test_chat_by_id(self, chat1):
chat2 = chat1.account.get_chat_by_id(chat1.id)
assert chat2 == chat1
with pytest.raises(ValueError):
chat1.account.get_chat_by_id(123123)
def test_chat_idempotent(self, chat1, ac1):
contact1 = chat1.get_contacts()[0]
chat2 = contact1.create_chat()
assert chat2.id == chat1.id
assert chat2.get_name() == chat1.get_name()
assert chat1 == chat2
assert not (chat1 != chat2)
for ichat in ac1.get_chats():
if ichat.id == chat1.id:
break
else:
pytest.fail("could not find chat")
def test_group_chat_add_second_account(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
chat = ac1.create_group_chat(name="title1")
with pytest.raises(ValueError):
chat.add_contact(ac2.get_self_contact())
contact = chat.add_contact(ac2)
assert contact.addr == ac2.get_config("addr")
assert contact.name == ac2.get_config("displayname")
assert contact.account == ac1
chat.remove_contact(ac2)
def test_group_chat_creation(self, ac1):
contact1 = ac1.create_contact("some1@example.org", name="some1")
contact2 = ac1.create_contact("some2@example.org", name="some2")
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
assert chat.get_name() == "title1"
assert contact1 in chat.get_contacts()
assert contact2 in chat.get_contacts()
assert not chat.is_promoted()
chat.set_name("title2")
assert chat.get_name() == "title2"
d = chat.get_summary()
print(d)
assert d["id"] == chat.id
assert d["type"] == chat.get_type()
assert d["name"] == chat.get_name()
assert d["archived"] == chat.is_archived()
# assert d["param"] == chat.param
assert d["color"] == chat.get_color()
assert d["profile_image"] == "" if chat.get_profile_image() is None else chat.get_profile_image()
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1):
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "abc %1$s xyz %2$s")
ac1._evtracker.consume_events()
with pytest.raises(ValueError):
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(500, "xyz %1$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
chat = ac1.create_group_chat(name="homework", contacts=[])
assert chat.get_name() == "homework"
chat.send_text("Now we have a group for homework")
assert chat.is_promoted()
chat.set_name("Homework")
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
@pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified):
ac2 = acfactory.get_pseudo_configured_account()
chat = ac1.create_group_chat(name="title1", verified=verified)
assert chat.is_group()
qr = chat.get_join_qr()
assert ac2.check_qr(qr).is_ask_verifygroup
def test_removing_blocked_user_from_group(self, ac1, lp):
"""
Test that blocked contact is not unblocked when removed from a group.
See https://github.com/deltachat/deltachat-core-rust/issues/2030
"""
lp.sec("Create a group chat with a contact")
contact = ac1.create_contact("some1@example.org")
group = ac1.create_group_chat("title", contacts=[contact])
group.send_text("First group message")
lp.sec("ac1 blocks contact")
contact.block()
assert contact.is_blocked()
lp.sec("ac1 removes contact from their group")
group.remove_contact(contact)
assert contact.is_blocked()
lp.sec("ac1 adding blocked contact unblocks it")
group.add_contact(contact)
assert not contact.is_blocked()
def test_get_set_profile_image_simple(self, ac1, data):
chat = ac1.create_group_chat(name="title1")
p = data.get_path("d.png")
chat.set_profile_image(p)
p2 = chat.get_profile_image()
assert open(p, "rb").read() == open(p2, "rb").read()
chat.remove_profile_image()
assert chat.get_profile_image() is None
def test_mute(self, ac1):
chat = ac1.create_group_chat(name="title1")
assert not chat.is_muted()
assert chat.get_mute_duration() == 0
chat.mute()
assert chat.is_muted()
assert chat.get_mute_duration() == -1
chat.unmute()
assert not chat.is_muted()
chat.mute(50)
assert chat.is_muted()
assert chat.get_mute_duration() <= 50
with pytest.raises(ValueError):
chat.mute(-51)
# Regression test, this caused Rust panic previously
chat.mute(2**63 - 1)
assert chat.is_muted()
assert chat.get_mute_duration() == -1
def test_delete_and_send_fails(self, ac1, chat1):
chat1.delete()
ac1._evtracker.wait_next_messages_changed()
with pytest.raises(ValueError):
chat1.send_text("msg1")
def test_prepare_message_and_send(self, ac1, chat1):
msg = chat1.prepare_message(Message.new_empty(chat1.account, "text"))
msg.set_text("hello world")
assert msg.text == "hello world"
assert msg.id > 0
chat1.send_prepared(msg)
assert "Sent" in msg.get_message_info()
str(msg)
repr(msg)
assert msg == ac1.get_message_by_id(msg.id)
def test_prepare_file(self, ac1, chat1):
blobdir = ac1.get_blobdir()
p = os.path.join(blobdir, "somedata.txt")
with open(p, "w") as f:
f.write("some data")
message = chat1.prepare_message_file(p)
assert message.id > 0
message.set_text("hello world")
assert message.is_out_preparing()
assert message.text == "hello world"
chat1.send_prepared(message)
assert "Sent" in message.get_message_info()
def test_message_eq_contains(self, chat1):
msg = chat1.send_text("msg1")
assert msg in chat1.get_messages()
assert not (msg not in chat1.get_messages())
str(msg)
repr(msg)
def test_message_send_text(self, chat1):
msg = chat1.send_text("msg1")
assert msg
assert msg.is_text()
assert not msg.is_audio()
assert not msg.is_video()
assert not msg.is_gif()
assert not msg.is_file()
assert not msg.is_image()
assert not msg.is_in_fresh()
assert not msg.is_in_noticed()
assert not msg.is_in_seen()
assert msg.is_out_pending()
assert not msg.is_out_failed()
assert not msg.is_out_delivered()
assert not msg.is_out_mdn_received()
def test_message_image(self, chat1, data, lp):
with pytest.raises(ValueError):
chat1.send_image(path="notexists")
fn = data.get_path("d.png")
lp.sec("sending image")
chat1.account._evtracker.consume_events()
msg = chat1.send_image(fn)
chat1.account._evtracker.get_matching("DC_EVENT_NEW_BLOB_FILE")
assert msg.is_image()
assert msg
assert msg.id > 0
assert os.path.exists(msg.filename)
assert msg.filemime == "image/png"
@pytest.mark.parametrize("typein,typeout", [
(None, "application/octet-stream"),
("text/plain", "text/plain"),
("image/png", "image/png"),
])
def test_message_file(self, ac1, chat1, data, lp, typein, typeout):
lp.sec("sending file")
fn = data.get_path("r.txt")
msg = chat1.send_file(fn, typein)
assert msg
assert msg.id > 0
assert msg.is_file()
assert os.path.exists(msg.filename)
assert msg.filename.endswith(msg.basename)
assert msg.filemime == typeout
msg2 = chat1.send_file(fn, typein)
assert msg2 != msg
assert msg2.filename != msg.filename
def test_create_contact(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
email = "hello <hello@example.org>"
contact1 = ac1.create_contact(email)
assert contact1.addr == "hello@example.org"
assert contact1.name == "hello"
contact1 = ac1.create_contact(email, name="world")
assert contact1.name == "world"
contact2 = ac1.create_contact("display1 <x@example.org>", "real")
assert contact2.name == "real"
def test_create_chat_simple(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
contact1.create_chat().send_text("hello")
def test_chat_message_distinctions(self, ac1, chat1):
past1s = datetime.now(timezone.utc) - timedelta(seconds=1)
msg = chat1.send_text("msg1")
ts = msg.time_sent
assert msg.time_received is None
assert ts.strftime("Y")
assert past1s < ts
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
def test_set_config_after_configure_is_forbidden(self, ac1):
assert ac1.get_config("mail_pw")
assert ac1.is_configured()
with pytest.raises(ValueError):
ac1.set_config("addr", "123@example.org")
def test_import_export_one_contact(self, acfactory, tmpdir):
backupdir = tmpdir.mkdir("backup")
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
bin = tmpdir.join("some.bin")
with bin.open("w") as f:
f.write("\00123" * 10000)
msg = chat.send_file(bin.strpath)
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
assert not backupdir.listdir()
ac1.stop_io()
path = ac1.export_all(backupdir.strpath)
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account()
ac2.import_all(path)
contacts = ac2.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == "some1@example.org"
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_set_get_draft(self, chat1):
msg = Message.new_empty(chat1.account, "text")
msg1 = chat1.prepare_message(msg)
msg1.set_text("hello")
chat1.set_draft(msg1)
msg1.set_text("obsolete")
msg2 = chat1.get_draft()
assert msg2.text == "hello"
chat1.set_draft(None)
assert chat1.get_draft() is None
def test_qr_setup_contact(self, acfactory, lp):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
qr = ac1.get_setup_contact_qr()
assert qr.startswith("OPENPGP4FPR:")
res = ac2.check_qr(qr)
assert res.is_ask_verifycontact()
assert not res.is_ask_verifygroup()
assert res.contact_id == 10
def test_quote(self, chat1):
"""Offline quoting test"""
msg = Message.new_empty(chat1.account, "text")
msg.set_text("Multi\nline\nmessage")
assert msg.quoted_text is None
# Prepare message to assign it a Message-Id.
# Messages without Message-Id cannot be quoted.
msg = chat1.prepare_message(msg)
reply_msg = Message.new_empty(chat1.account, "text")
reply_msg.set_text("reply")
reply_msg.quote = msg
assert reply_msg.quoted_text == "Multi\nline\nmessage"
def test_group_chat_many_members_add_remove(self, ac1, lp):
lp.sec("ac1: creating group chat with 10 other members")
chat = ac1.create_group_chat(name="title1")
# promote chat
chat.send_text("hello")
assert chat.is_promoted()
# activate local plugin
in_list = []
class InPlugin:
@account_hookimpl
def ac_member_added(self, chat, contact, actor):
in_list.append(("added", chat, contact, actor))
@account_hookimpl
def ac_member_removed(self, chat, contact, actor):
in_list.append(("removed", chat, contact, actor))
ac1.add_account_plugin(InPlugin())
# perform add contact many times
contacts = []
for i in range(10):
lp.sec("create contact")
contact = ac1.create_contact("some{}@example.org".format(i))
contacts.append(contact)
lp.sec("add contact")
chat.add_contact(contact)
assert chat.num_contacts() == 11
# let's make sure the events perform plugin hooks
def wait_events(cond):
now = time.time()
while time.time() < now + 5:
if cond():
break
time.sleep(0.1)
else:
pytest.fail("failed to get events")
wait_events(lambda: len(in_list) == 10)
assert len(in_list) == 10
chat_contacts = chat.get_contacts()
for in_cmd, in_chat, in_contact, in_actor in in_list:
assert in_cmd == "added"
assert in_chat == chat
assert in_contact in chat_contacts
assert in_actor is None
chat_contacts.remove(in_contact)
assert chat_contacts[0].id == 1 # self contact
in_list[:] = []
lp.sec("ac1: removing two contacts and checking things are right")
chat.remove_contact(contacts[9])
chat.remove_contact(contacts[3])
assert chat.num_contacts() == 9
wait_events(lambda: len(in_list) == 2)
assert len(in_list) == 2
assert in_list[0][0] == "removed"
assert in_list[0][1] == chat
assert in_list[0][2] == contacts[9]
assert in_list[1][0] == "removed"
assert in_list[1][1] == chat
assert in_list[1][2] == contacts[3]
def test_audit_log_view_without_daymarker(self, ac1, lp):
lp.sec("ac1: test audit log (show only system messages)")
chat = ac1.create_group_chat(name="audit log sample data")
# promote chat
chat.send_text("hello")
assert chat.is_promoted()
lp.sec("create test data")
chat.add_contact(ac1.create_contact("some-1@example.org"))
chat.set_name("audit log test group")
chat.send_text("a message in between")
lp.sec("check message count of all messages")
assert len(chat.get_messages()) == 4
lp.sec("check message count of only system messages (without daymarkers)")
dc_array = ffi.gc(
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0),
lib.dc_array_unref
)
assert len(list(iter_array(dc_array, lambda x: x))) == 2

View File

@@ -0,0 +1,215 @@
import os
from queue import Queue
from deltachat import capi, cutil, const
from deltachat import register_global_plugin
from deltachat.hookspec import global_hookimpl
from deltachat.capi import ffi
from deltachat.capi import lib
from deltachat.testplugin import ACSetup, create_dict_from_files_in_path, write_dict_to_dir
# from deltachat.account import EventLogger
class TestACSetup:
def test_cache_writing(self, tmp_path):
base = tmp_path.joinpath("hello")
base.mkdir()
d1 = base.joinpath("dir1")
d1.mkdir()
d1.joinpath("file1").write_bytes(b'content1')
d2 = d1.joinpath("dir2")
d2.mkdir()
d2.joinpath("file2").write_bytes(b"123")
d = create_dict_from_files_in_path(base)
newbase = tmp_path.joinpath("other")
write_dict_to_dir(d, newbase)
assert newbase.joinpath("dir1", "dir2", "file2").exists()
assert newbase.joinpath("dir1", "file1").exists()
def test_basic_states(self, acfactory, monkeypatch, testprocess):
pc = ACSetup(init_time=0.0, testprocess=testprocess)
acc = acfactory.get_unconfigured_account()
monkeypatch.setattr(acc, "configure", lambda **kwargs: None)
pc.start_configure(acc)
assert pc._account2state[acc] == pc.CONFIGURING
pc._configured_events.put((acc, True))
monkeypatch.setattr(pc, "init_imap", lambda *args, **kwargs: None)
pc.wait_one_configured(acc)
assert pc._account2state[acc] == pc.CONFIGURED
monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None)
pc.bring_online()
assert pc._account2state[acc] == pc.IDLEREADY
def test_two_accounts_one_waited_all_started(self, monkeypatch, acfactory, testprocess):
pc = ACSetup(init_time=0.0, testprocess=testprocess)
monkeypatch.setattr(pc, "init_imap", lambda *args, **kwargs: None)
monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None)
ac1 = acfactory.get_unconfigured_account()
monkeypatch.setattr(ac1, "configure", lambda **kwargs: None)
pc.start_configure(ac1)
ac2 = acfactory.get_unconfigured_account()
monkeypatch.setattr(ac2, "configure", lambda **kwargs: None)
pc.start_configure(ac2)
assert pc._account2state[ac1] == pc.CONFIGURING
assert pc._account2state[ac2] == pc.CONFIGURING
pc._configured_events.put((ac1, True))
pc.wait_one_configured(ac1)
assert pc._account2state[ac1] == pc.CONFIGURED
assert pc._account2state[ac2] == pc.CONFIGURING
pc._configured_events.put((ac2, True))
pc.bring_online()
assert pc._account2state[ac1] == pc.IDLEREADY
assert pc._account2state[ac2] == pc.IDLEREADY
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmpdir):
ac1 = acfactory.get_pseudo_configured_account()
holder = acfactory._acsetup.testprocess
assert holder.cache_maybe_store_configured_db_files(ac1)
assert not holder.cache_maybe_store_configured_db_files(ac1)
acdir = tmpdir.mkdir("newaccount")
addr = ac1.get_config("addr")
target_db_path = acdir.join("db").strpath
assert holder.cache_maybe_retrieve_configured_db_files(addr, target_db_path)
assert len(os.listdir(acdir)) >= 2
def test_liveconfig_caching(acfactory, monkeypatch):
prod = [
{"addr": "1@example.org", "mail_pw": "123"},
]
acfactory._liveconfig_producer = iter(prod)
d1 = acfactory.get_next_liveconfig()
d1["hello"] = "world"
acfactory._liveconfig_producer = iter(prod)
d2 = acfactory.get_next_liveconfig()
assert "hello" not in d2
def test_empty_context():
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
capi.lib.dc_context_unref(ctx)
def test_dc_close_events(tmpdir, acfactory):
ac1 = acfactory.get_unconfigured_account()
# register after_shutdown function
shutdowns = Queue()
class ShutdownPlugin:
@global_hookimpl
def dc_account_after_shutdown(self, account):
assert account._dc_context is None
shutdowns.put(account)
register_global_plugin(ShutdownPlugin())
assert hasattr(ac1, "_dc_context")
ac1.shutdown()
shutdowns.get(timeout=2)
def test_wrong_db(tmpdir):
p = tmpdir.join("hello.db")
# write an invalid database file
p.write("x123" * 10)
context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
assert not lib.dc_context_is_open(context)
def test_empty_blobdir(tmpdir):
db_fname = tmpdir.join("hello.db")
# Apparently some client code expects this to be the same as passing NULL.
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""),
lib.dc_context_unref,
)
assert ctx != ffi.NULL
def test_event_defines():
assert const.DC_EVENT_INFO == 100
assert const.DC_CONTACT_ID_SELF
def test_sig():
sig = capi.lib.dc_event_has_string_data
assert not sig(const.DC_EVENT_MSGS_CHANGED)
assert sig(const.DC_EVENT_INFO)
assert sig(const.DC_EVENT_WARNING)
assert sig(const.DC_EVENT_ERROR)
assert sig(const.DC_EVENT_SMTP_CONNECTED)
assert sig(const.DC_EVENT_IMAP_CONNECTED)
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
def test_markseen_invalid_message_ids(acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.com", name="some1")
chat = contact1.create_chat()
chat.send_text("one messae")
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg_ids = [9]
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
ac1._evtracker.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
def test_get_special_message_id_returns_empty_message(acfactory):
ac1 = acfactory.get_pseudo_configured_account()
for i in range(1, 10):
msg = ac1.get_message_by_id(i)
assert msg.id == 0
def test_provider_info_none():
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
def test_get_info_open(tmpdir):
db_fname = tmpdir.join("test.db")
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
lib.dc_context_unref,
)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
assert 'deltachat_core_version' in info
assert 'database_dir' in info
def test_logged_hook_failure(acfactory):
ac1 = acfactory.get_pseudo_configured_account()
cap = []
ac1.log = cap.append
with ac1._event_thread.swallow_and_log_exception("some"):
0/0
assert cap
assert "some" in str(cap)
assert "ZeroDivisionError" in str(cap)
assert "Traceback" in str(cap)
def test_logged_ac_process_ffi_failure(acfactory):
from deltachat import account_hookimpl
ac1 = acfactory.get_pseudo_configured_account()
class FailPlugin:
@account_hookimpl
def ac_process_ffi_event(ffi_event):
0/0
cap = Queue()
ac1.log = cap.put
ac1.add_account_plugin(FailPlugin())
# cause any event eg contact added/changed
ac1.create_contact("something@example.org")
res = cap.get(timeout=10)
assert "ac_process_ffi_event" in res
assert "ZeroDivisionError" in res
assert "Traceback" in res

File diff suppressed because it is too large Load Diff

View File

@@ -1,105 +0,0 @@
from __future__ import print_function
from queue import Queue
from deltachat import capi, cutil, const
from deltachat import register_global_plugin
from deltachat.hookspec import global_hookimpl
from deltachat.capi import ffi
from deltachat.capi import lib
# from deltachat.account import EventLogger
def test_empty_context():
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
capi.lib.dc_context_unref(ctx)
def test_dc_close_events(tmpdir, acfactory):
ac1 = acfactory.get_unconfigured_account()
# register after_shutdown function
shutdowns = Queue()
class ShutdownPlugin:
@global_hookimpl
def dc_account_after_shutdown(self, account):
assert account._dc_context is None
shutdowns.put(account)
register_global_plugin(ShutdownPlugin())
assert hasattr(ac1, "_dc_context")
ac1.shutdown()
shutdowns.get(timeout=2)
def test_wrong_db(tmpdir):
p = tmpdir.join("hello.db")
# write an invalid database file
p.write("x123" * 10)
context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
assert not lib.dc_context_is_open(context)
def test_empty_blobdir(tmpdir):
db_fname = tmpdir.join("hello.db")
# Apparently some client code expects this to be the same as passing NULL.
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""),
lib.dc_context_unref,
)
assert ctx != ffi.NULL
def test_event_defines():
assert const.DC_EVENT_INFO == 100
assert const.DC_CONTACT_ID_SELF
def test_sig():
sig = capi.lib.dc_event_has_string_data
assert not sig(const.DC_EVENT_MSGS_CHANGED)
assert sig(const.DC_EVENT_INFO)
assert sig(const.DC_EVENT_WARNING)
assert sig(const.DC_EVENT_ERROR)
assert sig(const.DC_EVENT_SMTP_CONNECTED)
assert sig(const.DC_EVENT_IMAP_CONNECTED)
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
def test_markseen_invalid_message_ids(acfactory):
ac1 = acfactory.get_configured_offline_account()
contact1 = ac1.create_contact("some1@example.com", name="some1")
chat = contact1.create_chat()
chat.send_text("one messae")
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg_ids = [9]
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
ac1._evtracker.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
def test_get_special_message_id_returns_empty_message(acfactory):
ac1 = acfactory.get_configured_offline_account()
for i in range(1, 10):
msg = ac1.get_message_by_id(i)
assert msg.id == 0
def test_provider_info_none():
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
def test_get_info_open(tmpdir):
db_fname = tmpdir.join("test.db")
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
lib.dc_context_unref,
)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
assert 'deltachat_core_version' in info
assert 'database_dir' in info

View File

@@ -8,7 +8,7 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
@@ -78,8 +78,8 @@ commands =
addopts = -v -ra --strict-markers
norecursedirs = .tox
xfail_strict=true
timeout = 90
timeout_method = thread
timeout = 150
timeout_func_only = True
markers =
ignored: ignore this test in default test runs, use --ignored to run.

View File

@@ -1 +1 @@
1.54.0
1.61.0

View File

@@ -8,7 +8,7 @@ set -e -x
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.54.0
RUST_VERSION=1.61.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"

View File

@@ -8,7 +8,7 @@ set -e -x
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.54.0
RUST_VERSION=1.61.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"

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
import os
import sys
import json
import re
import pathlib
import subprocess
@@ -41,6 +41,24 @@ def replace_toml_version(relpath, newversion):
os.rename(tmp_path, str(p))
def read_json_version(relpath):
p = pathlib.Path("package.json")
assert p.exists()
with open(p, "r") as f:
json_data = json.loads(f.read())
return json_data["version"]
def update_package_json(newversion):
p = pathlib.Path("package.json")
assert p.exists()
with open(p, "r") as f:
json_data = json.loads(f.read())
json_data["version"] = newversion
with open(p, "w") as f:
f.write(json.dumps(json_data, sort_keys=True, indent=2))
def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
@@ -52,6 +70,7 @@ def main():
print()
for x in toml_list:
print("{}: {}".format(x, read_toml_version(x)))
print("package.json:", str(read_json_version("package.json")))
print()
raise SystemExit("need argument: new version, example: 1.25.0")
@@ -74,6 +93,7 @@ def main():
replace_toml_version("Cargo.toml", newversion)
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
update_package_json(newversion)
print("running cargo check")
subprocess.call(["cargo", "check"])

View File

@@ -54,7 +54,11 @@ impl Accounts {
ensure!(dir.exists().await, "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists().await, "accounts.toml does not exist");
ensure!(
config_file.exists().await,
"{:?} does not exist",
config_file
);
let config = Config::from_file(config_file)
.await
@@ -137,16 +141,35 @@ impl Accounts {
/// Remove an account.
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
let ctx = self.accounts.remove(&id);
ensure!(ctx.is_some(), "no account with this id: {}", id);
let ctx = ctx.unwrap();
let ctx = self
.accounts
.remove(&id)
.with_context(|| format!("no account with id {}", id))?;
ctx.stop_io().await;
drop(ctx);
if let Some(cfg) = self.config.get_account(id).await {
fs::remove_dir_all(async_std::path::PathBuf::from(&cfg.dir))
.await
.context("failed to remove account data")?;
// Spend up to 1 minute trying to remove the files.
// Files may remain locked up to 30 seconds due to r2d2 bug:
// https://github.com/sfackler/r2d2/issues/99
let mut counter = 0;
loop {
counter += 1;
if let Err(err) = fs::remove_dir_all(async_std::path::PathBuf::from(&cfg.dir))
.await
.context("failed to remove account data")
{
if counter > 60 {
return Err(err);
}
// Wait 1 second and try again.
async_std::task::sleep(std::time::Duration::from_millis(1000)).await;
} else {
break;
}
}
}
self.config.remove_account(id).await?;

View File

@@ -3,29 +3,26 @@
use core::cmp::max;
use std::ffi::OsStr;
use std::fmt;
use std::io::Cursor;
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 anyhow::{format_err, Context as _, Error};
use image::{DynamicImage, ImageFormat};
use num_traits::FromPrimitive;
use thiserror::Error;
use crate::config::Config;
use crate::constants::{
MediaQuality, Viewtype, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE,
WORSE_IMAGE_SIZE,
MediaQuality, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE, WORSE_IMAGE_SIZE,
};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::message;
use crate::message::Viewtype;
/// Represents a file in the blob directory.
///
@@ -292,7 +289,7 @@ impl<'a> BlobObject<'a> {
/// Returns the filename of the blob.
pub fn as_file_name(&self) -> &str {
self.name.rsplitn(2, '/').next().unwrap()
self.name.rsplit('/').next().unwrap_or_default()
}
/// The path relative in the blob directory.
@@ -305,7 +302,7 @@ impl<'a> BlobObject<'a> {
/// If a blob's filename has an extension, it is always guaranteed
/// to be lowercase.
pub fn suffix(&self) -> Option<&str> {
let ext = self.name.rsplitn(2, '.').next();
let ext = self.name.rsplit('.').next();
if ext == Some(&self.name) {
None
} else {
@@ -348,13 +345,30 @@ impl<'a> BlobObject<'a> {
};
let clean = sanitize_filename::sanitize_with_options(name, opts);
// Let's take the tricky filename
// "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example.
// Split it into "file" and "with_lots_of_characters_behind_point_and_double_ending.tar.gz":
let mut iter = clean.splitn(2, '.');
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
let ext: String = iter.next().unwrap_or_default().chars().take(32).collect();
// stem == "file"
let ext_chars = iter.next().unwrap_or_default().chars();
let ext: String = ext_chars
.rev()
.take(32)
.collect::<Vec<_>>()
.iter()
.rev()
.collect();
// ext == "d_point_and_double_ending.tar.gz"
if ext.is_empty() {
(stem, "".to_string())
} else {
(stem, format!(".{}", ext).to_lowercase())
// Return ("file", ".d_point_and_double_ending.tar.gz")
// which is not perfect but acceptable.
}
}
@@ -449,7 +463,8 @@ impl<'a> BlobObject<'a> {
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
encoded.clear();
img.write_to(encoded, image::ImageFormat::Jpeg)?;
let mut buf = Cursor::new(encoded);
img.write_to(&mut buf, image::ImageFormat::Jpeg)?;
Ok(())
}
fn encoded_img_exceeds_bytes(
@@ -619,16 +634,14 @@ pub enum BlobError {
mod tests {
use fs::File;
use super::*;
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::{
chat,
message::Message,
test_utils::{self, TestContext},
};
use anyhow::Result;
use image::Pixel;
use image::{GenericImageView, Pixel};
use crate::chat::{self, create_group_chat, ProtectionStatus};
use crate::message::Message;
use crate::test_utils::{self, TestContext};
use super::*;
#[async_std::test]
async fn test_create() {
@@ -927,7 +940,7 @@ mod tests {
}
#[async_std::test]
async fn test_recode_image() {
async fn test_recode_image_1() {
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)
@@ -944,7 +957,10 @@ mod tests {
)
.await
.unwrap();
}
#[async_std::test]
async fn test_recode_image_2() {
// 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(
@@ -960,22 +976,29 @@ mod tests {
.unwrap();
assert_correct_rotation(&img_rotated);
let mut bytes = vec![];
let mut buf = Cursor::new(vec![]);
img_rotated
.write_to(&mut bytes, image::ImageFormat::Jpeg)
.write_to(&mut buf, 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 bytes = buf.into_inner();
// Do this in parallel to speed up the test a bit
// (it still takes very long though)
let bytes2 = bytes.clone();
let join_handle = async_std::task::spawn(async move {
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes2,
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"),
@@ -990,6 +1013,11 @@ mod tests {
.unwrap();
assert_correct_rotation(&img_rotated);
join_handle.await;
}
#[async_std::test]
async fn test_recode_image_3() {
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
@@ -1055,8 +1083,7 @@ mod tests {
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;
let bob_msg = bob.recv_msg(&sent).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();

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,11 @@ use anyhow::{ensure, Context as _, Result};
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
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,
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_GCL_ADD_ALLDONE_HINT,
DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
};
use crate::contact::Contact;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::ephemeral::delete_expired_messages;
use crate::message::{Message, MessageState, MsgId};
use crate::stock_str;
use crate::summary::Summary;
@@ -85,19 +83,13 @@ impl Chatlist {
context: &Context,
listflags: usize,
query: Option<&str>,
query_contact_id: Option<u32>,
query_contact_id: Option<ContactId>,
) -> Result<Self> {
let flag_archived_only = 0 != listflags & DC_GCL_ARCHIVED_ONLY;
let flag_for_forwarding = 0 != listflags & DC_GCL_FOR_FORWARDING;
let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS;
let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT;
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
// messages get deleted to avoid reloading the same chatlist.
if let Err(err) = delete_expired_messages(context).await {
warn!(context, "Failed to hide expired messages: {}", err);
}
let mut add_archived_link_item = false;
let process_row = |row: &rusqlite::Row| {
@@ -112,7 +104,7 @@ impl Chatlist {
};
let skip_id = if flag_for_forwarding {
ChatId::lookup_by_contact(context, DC_CONTACT_ID_DEVICE)
ChatId::lookup_by_contact(context, ContactId::DEVICE)
.await?
.unwrap_or_default()
} else {
@@ -147,7 +139,7 @@ impl Chatlist {
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;",
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
paramsv![MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned],
process_row,
process_rows,
).await?
@@ -216,7 +208,7 @@ impl Chatlist {
} else {
// show normal chatlist
let sort_id_up = if flag_for_forwarding {
ChatId::lookup_by_contact(context, DC_CONTACT_ID_SELF)
ChatId::lookup_by_contact(context, ContactId::SELF)
.await?
.unwrap_or_default()
} else {
@@ -326,7 +318,7 @@ impl Chatlist {
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 {
if lastmsg.from_id == ContactId::SELF {
(Some(lastmsg), None)
} else {
match chat.typ {
@@ -343,7 +335,7 @@ impl Chatlist {
if chat.id.is_archived_link() {
Ok(Default::default())
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != DC_CONTACT_ID_UNDEFINED) {
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != ContactId::UNDEFINED) {
Ok(Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await)
} else {
Ok(Summary {
@@ -356,6 +348,10 @@ impl Chatlist {
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
self.ids.iter().position(|(chat_id, _)| chat_id == &id)
}
pub fn iter(&self) -> impl Iterator<Item = &(ChatId, Option<MsgId>)> {
self.ids.iter()
}
}
/// Returns the number of archived chats
@@ -375,8 +371,8 @@ mod tests {
use super::*;
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::Viewtype;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
@@ -507,7 +503,6 @@ mod tests {
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
\n\
hello foo\n",
"INBOX",
false,
)
.await?;
@@ -568,7 +563,6 @@ mod tests {
Date: Sun, 22 Mar 2021 22:38:57 +0000\n\
\n\
hello foo\n",
"INBOX",
false,
)
.await?;

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