mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
Merge remote-tracking branch 'origin/master' into feat/async-jobs
This commit is contained in:
@@ -7,6 +7,10 @@ executors:
|
|||||||
doxygen:
|
doxygen:
|
||||||
docker:
|
docker:
|
||||||
- image: hrektts/doxygen
|
- image: hrektts/doxygen
|
||||||
|
python:
|
||||||
|
docker:
|
||||||
|
- image: 3.7.7-stretch
|
||||||
|
|
||||||
|
|
||||||
restore-workspace: &restore-workspace
|
restore-workspace: &restore-workspace
|
||||||
attach_workspace:
|
attach_workspace:
|
||||||
|
|||||||
68
CHANGELOG.md
68
CHANGELOG.md
@@ -1,5 +1,73 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.32.0
|
||||||
|
|
||||||
|
- fix endless loop when trying to download messages with bad RFC Message-ID,
|
||||||
|
also be more reliable on similar errors #1463 #1466 #1462
|
||||||
|
|
||||||
|
- fix bug with comma in contact request #1438
|
||||||
|
|
||||||
|
- do not refer to hidden messages on replies #1459
|
||||||
|
|
||||||
|
- improve error handling #1468 #1465 #1464
|
||||||
|
|
||||||
|
|
||||||
|
## 1.31.0
|
||||||
|
|
||||||
|
- always describe the context of the displayed error #1451
|
||||||
|
|
||||||
|
- do not emit `DC_EVENT_ERROR` when message sending fails;
|
||||||
|
`dc_msg_get_state()` and `dc_get_msg_info()` are sufficient #1451
|
||||||
|
|
||||||
|
- new config-option `media_quality` #1449
|
||||||
|
|
||||||
|
- try over if writing message to database fails #1447
|
||||||
|
|
||||||
|
|
||||||
|
## 1.30.0
|
||||||
|
|
||||||
|
- expunge deleted messages #1440
|
||||||
|
|
||||||
|
- do not send `DC_EVENT_MSGS_CHANGED|INCOMING_MSG` on hidden messages #1439
|
||||||
|
|
||||||
|
|
||||||
|
## 1.29.0
|
||||||
|
|
||||||
|
- new config options `delete_device_after` and `delete_server_after`,
|
||||||
|
each taking an amount of seconds after which messages
|
||||||
|
are deleted from the device and/or the server #1310 #1335 #1411 #1417 #1423
|
||||||
|
|
||||||
|
- new api `dc_estimate_deletion_cnt()` to estimate the effect
|
||||||
|
of `delete_device_after` and `delete_server_after`
|
||||||
|
|
||||||
|
- use Ed25519 keys by default, these keys are much shorter
|
||||||
|
than RSA keys, which results in saving traffic and speed improvements #1362
|
||||||
|
|
||||||
|
- improve message ellipsizing #1397 #1430
|
||||||
|
|
||||||
|
- emit `DC_EVENT_ERROR_NETWORK` also on smtp-errors #1378
|
||||||
|
|
||||||
|
- do not show badly formatted non-delta-messages as empty #1384
|
||||||
|
|
||||||
|
- try over SMTP on potentially recoverable error 5.5.0 #1379
|
||||||
|
|
||||||
|
- remove device-chat from forward-to-chat-list #1367
|
||||||
|
|
||||||
|
- improve group-handling #1368
|
||||||
|
|
||||||
|
- `dc_get_info()` returns uptime (how long the context is in use)
|
||||||
|
|
||||||
|
- python improvements and adaptions #1408 #1415
|
||||||
|
|
||||||
|
- log to the stdout and stderr in tests #1416
|
||||||
|
|
||||||
|
- refactoring, code improvements #1363 #1365 #1366 #1370 #1375 #1389 #1390 #1418 #1419
|
||||||
|
|
||||||
|
- removed api: `dc_chat_get_subtitle()`, `dc_get_version_str()`, `dc_array_add_id()`
|
||||||
|
|
||||||
|
- removed events: `DC_EVENT_MEMBER_ADDED`, `DC_EVENT_MEMBER_REMOVED`
|
||||||
|
|
||||||
|
|
||||||
## 1.28.0
|
## 1.28.0
|
||||||
|
|
||||||
- new flag DC_GCL_FOR_FORWARDING for dc_get_chatlist()
|
- new flag DC_GCL_FOR_FORWARDING for dc_get_chatlist()
|
||||||
|
|||||||
1032
Cargo.lock
generated
1032
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.28.0"
|
version = "1.32.0"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
@@ -25,15 +25,13 @@ email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
|||||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||||
async-imap = { git = "https://github.com/async-email/async-imap", branch = "feat/send" }
|
async-imap = { git = "https://github.com/async-email/async-imap", branch = "feat/send" }
|
||||||
async-native-tls = "0.3.1"
|
async-native-tls = "0.3.1"
|
||||||
async-std = { git = "https://github.com/async-rs/async-std", version = "1.5", features = ["unstable"] }
|
async-std = { git = "https://github.com/async-rs/async-std", rev = "3ff9e98f20a193eb63e43fb9d71f9d60c33f6d58", features = ["unstable"] }
|
||||||
base64 = "0.11"
|
base64 = "0.11"
|
||||||
charset = "0.1"
|
charset = "0.1"
|
||||||
percent-encoding = "2.0"
|
percent-encoding = "2.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
chrono = "0.4.6"
|
chrono = "0.4.6"
|
||||||
failure = "0.1.5"
|
|
||||||
failure_derive = "0.1.5"
|
|
||||||
indexmap = "1.3.0"
|
indexmap = "1.3.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
regex = "1.1.6"
|
regex = "1.1.6"
|
||||||
@@ -59,19 +57,21 @@ native-tls = "0.2.3"
|
|||||||
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
|
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
|
||||||
futures = "0.3.4"
|
futures = "0.3.4"
|
||||||
crossbeam-queue = "0.2.1"
|
crossbeam-queue = "0.2.1"
|
||||||
|
thiserror = "1.0.14"
|
||||||
|
anyhow = "1.0.28"
|
||||||
|
|
||||||
pretty_env_logger = { version = "0.3.1", optional = true }
|
pretty_env_logger = { version = "0.3.1", optional = true }
|
||||||
log = {version = "0.4.8", optional = true }
|
log = {version = "0.4.8", optional = true }
|
||||||
rustyline = { version = "4.1.0", optional = true }
|
rustyline = { version = "4.1.0", optional = true }
|
||||||
ansi_term = { version = "0.12.1", optional = true }
|
ansi_term = { version = "0.12.1", optional = true }
|
||||||
|
async-trait = "0.1.31"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
pretty_assertions = "0.6.1"
|
pretty_assertions = "0.6.1"
|
||||||
pretty_env_logger = "0.3.0"
|
pretty_env_logger = "0.3.0"
|
||||||
proptest = "0.9.4"
|
proptest = "0.9.4"
|
||||||
async-std = { git = "https://github.com/async-rs/async-std", version = "1.5", features = ["unstable", "attributes"] }
|
async-std = { git = "https://github.com/async-rs/async-std", rev = "3ff9e98f20a193eb63e43fb9d71f9d60c33f6d58", features = ["unstable", "attributes"] }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ $ curl https://sh.rustup.rs -sSf | sh
|
|||||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ RUST_LOG=info cargo run --example repl --features repl -- /path/to/db
|
$ RUST_LOG=info cargo run --example repl --features repl -- ~/deltachat-db
|
||||||
```
|
```
|
||||||
|
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
||||||
|
|
||||||
Configure your account (if not already configured):
|
Configure your account (if not already configured):
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ echo -----------------------
|
|||||||
# Bundle external shared libraries into the wheels
|
# Bundle external shared libraries into the wheels
|
||||||
pushd $WHEELHOUSEDIR
|
pushd $WHEELHOUSEDIR
|
||||||
|
|
||||||
pip3 install -U pip
|
pip3 install -U pip setuptools
|
||||||
pip3 install devpi-client
|
pip3 install devpi-client
|
||||||
devpi use https://m.devpi.net
|
devpi use https://m.devpi.net
|
||||||
devpi login dc --password $DEVPI_LOGIN
|
devpi login dc --password $DEVPI_LOGIN
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ def run():
|
|||||||
projectnames = get_projectnames(baseurl, username, indexname)
|
projectnames = get_projectnames(baseurl, username, indexname)
|
||||||
if indexname == "master" or not indexname:
|
if indexname == "master" or not indexname:
|
||||||
continue
|
continue
|
||||||
assert projectnames == ["deltachat"]
|
clear_index = not projectnames
|
||||||
for projectname in projectnames:
|
for projectname in projectnames:
|
||||||
dates = get_release_dates(baseurl, username, indexname, projectname)
|
dates = get_release_dates(baseurl, username, indexname, projectname)
|
||||||
if not dates:
|
if not dates:
|
||||||
@@ -60,8 +60,11 @@ def run():
|
|||||||
date = datetime.datetime(*max(dates))
|
date = datetime.datetime(*max(dates))
|
||||||
if (datetime.datetime.now() - date) > datetime.timedelta(days=MAXDAYS):
|
if (datetime.datetime.now() - date) > datetime.timedelta(days=MAXDAYS):
|
||||||
assert username and indexname
|
assert username and indexname
|
||||||
url = baseurl + username + "/" + indexname
|
clear_index = True
|
||||||
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
|
break
|
||||||
|
if clear_index:
|
||||||
|
url = baseurl + username + "/" + indexname
|
||||||
|
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.28.0"
|
version = "1.32.0"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@@ -19,9 +19,10 @@ deltachat = { path = "../", default-features = false }
|
|||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
human-panic = "1.0.1"
|
human-panic = "1.0.1"
|
||||||
num-traits = "0.2.6"
|
num-traits = "0.2.6"
|
||||||
failure = "0.1.6"
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
async-std = "1.5.0"
|
async-std = "1.5.0"
|
||||||
|
anyhow = "1.0.28"
|
||||||
|
thiserror = "1.0.14"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored", "nightly"]
|
default = ["vendored", "nightly"]
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ typedef struct _dc_event dc_event_t;
|
|||||||
* uintptr_t event_handler_func(dc_context_t* context, int event,
|
* uintptr_t event_handler_func(dc_context_t* context, int event,
|
||||||
* uintptr_t data1, uintptr_t data2)
|
* uintptr_t data1, uintptr_t data2)
|
||||||
* {
|
* {
|
||||||
* return 0; // for unhandled events, it is always safe to return 0
|
* return 0;
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* dc_context_t* context = dc_context_new(event_handler_func, NULL, NULL);
|
* dc_context_t* context = dc_context_new(event_handler_func, NULL, NULL);
|
||||||
@@ -236,7 +236,7 @@ void dc_event_unref (dc_event_t* event);
|
|||||||
* otherwise!
|
* otherwise!
|
||||||
* - The callback SHOULD return _fast_, for GUI updates etc. you should
|
* - The callback SHOULD return _fast_, for GUI updates etc. you should
|
||||||
* post yourself an asynchronous message to your GUI thread, if needed.
|
* post yourself an asynchronous message to your GUI thread, if needed.
|
||||||
* - If not mentioned otherweise, the callback should return 0.
|
* - events do not expect a return value, just always return 0.
|
||||||
* @param userdata can be used by the client for any purpuse. He finds it
|
* @param userdata can be used by the client for any purpuse. He finds it
|
||||||
* later in dc_get_userdata().
|
* later in dc_get_userdata().
|
||||||
* @param os_name is only for decorative use
|
* @param os_name is only for decorative use
|
||||||
@@ -349,7 +349,8 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||||
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way eg. using CC, defaults to empty
|
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way eg. using CC, defaults to empty
|
||||||
* - `selfstatus` = Own status to display eg. in email footers, defaults to a standard text
|
* - `selfstatus` = Own status to display eg. in email footers, defaults to a standard text
|
||||||
* - `selfavatar` = File containing avatar. Will be copied to blob directory.
|
* - `selfavatar` = File containing avatar. Will immediately be copied to the
|
||||||
|
* `blobdir`; the original image will not be needed anymore.
|
||||||
* NULL to remove the avatar.
|
* NULL to remove the avatar.
|
||||||
* It is planned for future versions
|
* It is planned for future versions
|
||||||
* to send this image together with the next messages.
|
* to send this image together with the next messages.
|
||||||
@@ -384,10 +385,20 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
* >=1=seconds, after which messages are deleted automatically from the device.
|
* >=1=seconds, after which messages are deleted automatically from the device.
|
||||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||||
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
|
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
|
||||||
|
* See also dc_estimate_deletion_cnt().
|
||||||
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
|
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
|
||||||
* >=1=seconds, after which messages are deleted automatically from the server.
|
* >=1=seconds, after which messages are deleted automatically from the server.
|
||||||
* "Saved messages" are deleted from the server as well as
|
* "Saved messages" are deleted from the server as well as
|
||||||
* emails matching the `show_emails` settings above, the UI should clearly point that out.
|
* emails matching the `show_emails` settings above, the UI should clearly point that out.
|
||||||
|
* See also dc_estimate_deletion_cnt().
|
||||||
|
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
|
||||||
|
* good outgoing images/videos/voice quality at reasonable sizes (default)
|
||||||
|
* DC_MEDIA_QUALITY_WORSE (1)
|
||||||
|
* allow worse images/videos/voice quality to gain smaller sizes,
|
||||||
|
* suitable for providers or areas known to have a bad connection.
|
||||||
|
* In contrast to other options, the implementation of this option is currently up to the UIs;
|
||||||
|
* this may change in future, however,
|
||||||
|
* having the option in the core allows provider-specific-defaults already today.
|
||||||
*
|
*
|
||||||
* If you want to retrieve a value, use dc_get_config().
|
* If you want to retrieve a value, use dc_get_config().
|
||||||
*
|
*
|
||||||
@@ -665,8 +676,10 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
|||||||
* if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
|
* if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
|
||||||
* the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
|
* the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
|
||||||
* chats
|
* chats
|
||||||
* - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist,
|
* - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
|
||||||
|
* and hides the "Device chat" and the deaddrop.
|
||||||
* typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
* typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
|
||||||
|
* to also hide the archive link.
|
||||||
* - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
* - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
|
||||||
* to the list (may be used eg. for selecting chats on forwarding, the flag is
|
* to the list (may be used eg. for selecting chats on forwarding, the flag is
|
||||||
* not needed when DC_GCL_ARCHIVED_ONLY is already set)
|
* not needed when DC_GCL_ARCHIVED_ONLY is already set)
|
||||||
@@ -1026,6 +1039,7 @@ int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t ch
|
|||||||
* by the dc_set_config()-options `delete_device_after` or `delete_server_after`.
|
* by the dc_set_config()-options `delete_device_after` or `delete_server_after`.
|
||||||
* This is typically used to show the estimated impact to the user before actually enabling ephemeral messages.
|
* This is typically used to show the estimated impact to the user before actually enabling ephemeral messages.
|
||||||
*
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
* @param context The context object as returned from dc_context_new().
|
* @param context The context object as returned from dc_context_new().
|
||||||
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
|
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
|
||||||
* @param seconds Count messages older than the given number of seconds.
|
* @param seconds Count messages older than the given number of seconds.
|
||||||
@@ -1329,8 +1343,10 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
|
|||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context as created by dc_context_new().
|
* @param context The context as created by dc_context_new().
|
||||||
* @param chat_id The chat ID to set the image for.
|
* @param chat_id The chat ID to set the image for.
|
||||||
* @param image Full path of the image to use as the group image. If you pass NULL here,
|
* @param image Full path of the image to use as the group image. The image will immediately be copied to the
|
||||||
* the group image is deleted (for promoted groups, all members are informed about this change anyway).
|
* `blobdir`; the original image will not be needed anymore.
|
||||||
|
* If you pass NULL here, the group image is deleted (for promoted groups, all members are informed about
|
||||||
|
* this change anyway).
|
||||||
* @return 1=success, 0=error
|
* @return 1=success, 0=error
|
||||||
*/
|
*/
|
||||||
int dc_set_chat_profile_image (dc_context_t* context, uint32_t chat_id, const char* image);
|
int dc_set_chat_profile_image (dc_context_t* context, uint32_t chat_id, const char* image);
|
||||||
@@ -1503,7 +1519,7 @@ int dc_may_be_valid_addr (const char* addr);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an e-mail address belongs to a known and unblocked contact.
|
* Check if an e-mail address belongs to a known and unblocked contact.
|
||||||
* Known and unblocked contacts will be returned by dc_get_contacts().
|
* To get a list of all known and unblocked contacts, use dc_get_contacts().
|
||||||
*
|
*
|
||||||
* To validate an e-mail address independently of the contact database
|
* To validate an e-mail address independently of the contact database
|
||||||
* use dc_may_be_valid_addr().
|
* use dc_may_be_valid_addr().
|
||||||
@@ -1511,7 +1527,8 @@ int dc_may_be_valid_addr (const char* addr);
|
|||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object as created by dc_context_new().
|
* @param context The context object as created by dc_context_new().
|
||||||
* @param addr The e-mail-address to check.
|
* @param addr The e-mail-address to check.
|
||||||
* @return 1=address is a contact in use, 0=address is not a contact in use.
|
* @return Contact ID of the contact belonging to the e-mail-address
|
||||||
|
* or 0 if there is no contact that is or was introduced by an accepted contact.
|
||||||
*/
|
*/
|
||||||
uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char* addr);
|
uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char* addr);
|
||||||
|
|
||||||
@@ -2543,19 +2560,6 @@ int dc_chat_get_type (const dc_chat_t* chat);
|
|||||||
char* dc_chat_get_name (const dc_chat_t* chat);
|
char* dc_chat_get_name (const dc_chat_t* chat);
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Get a subtitle for a chat. The subtitle is eg. the email-address or the
|
|
||||||
* number of group members.
|
|
||||||
*
|
|
||||||
* Deprecated function. Subtitles should be created in the ui
|
|
||||||
* where plural forms and other specials can be handled more gracefully.
|
|
||||||
*
|
|
||||||
* @param chat The chat object to calulate the subtitle for.
|
|
||||||
* @return Subtitle as a string. Must be released using dc_str_unref() after usage. Never NULL.
|
|
||||||
*/
|
|
||||||
char* dc_chat_get_subtitle (const dc_chat_t* chat);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the chat's profile image.
|
* Get the chat's profile image.
|
||||||
* For groups, this is the image set by any group member
|
* For groups, this is the image set by any group member
|
||||||
@@ -3876,8 +3880,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
*
|
*
|
||||||
* These constants are used as events
|
* These constants are used as events
|
||||||
* reported to the callback given to dc_context_new().
|
* reported to the callback given to dc_context_new().
|
||||||
* If you do not want to handle an event, it is always safe to return 0,
|
|
||||||
* so there is no need to add a "case" for every event.
|
|
||||||
*
|
*
|
||||||
* @addtogroup DC_EVENT
|
* @addtogroup DC_EVENT
|
||||||
* @{
|
* @{
|
||||||
@@ -3892,7 +3894,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data1 0
|
* @param data1 0
|
||||||
* @param data2 (const char*) Info string in english language.
|
* @param data2 (const char*) Info string in english language.
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_INFO 100
|
#define DC_EVENT_INFO 100
|
||||||
|
|
||||||
@@ -3903,7 +3904,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data1 0
|
* @param data1 0
|
||||||
* @param data2 (const char*) Info string in english language.
|
* @param data2 (const char*) Info string in english language.
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_SMTP_CONNECTED 101
|
#define DC_EVENT_SMTP_CONNECTED 101
|
||||||
|
|
||||||
@@ -3914,7 +3914,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data1 0
|
* @param data1 0
|
||||||
* @param data2 (const char*) Info string in english language.
|
* @param data2 (const char*) Info string in english language.
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_IMAP_CONNECTED 102
|
#define DC_EVENT_IMAP_CONNECTED 102
|
||||||
|
|
||||||
@@ -3924,7 +3923,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data1 0
|
* @param data1 0
|
||||||
* @param data2 (const char*) Info string in english language.
|
* @param data2 (const char*) Info string in english language.
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_SMTP_MESSAGE_SENT 103
|
#define DC_EVENT_SMTP_MESSAGE_SENT 103
|
||||||
|
|
||||||
@@ -3934,7 +3932,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data1 0
|
* @param data1 0
|
||||||
* @param data2 (const char*) Info string in english language.
|
* @param data2 (const char*) Info string in english language.
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_IMAP_MESSAGE_DELETED 104
|
#define DC_EVENT_IMAP_MESSAGE_DELETED 104
|
||||||
|
|
||||||
@@ -3944,7 +3941,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data1 0
|
* @param data1 0
|
||||||
* @param data2 (const char*) Info string in english language.
|
* @param data2 (const char*) Info string in english language.
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
|
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
|
||||||
|
|
||||||
@@ -3954,7 +3950,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data1 0
|
* @param data1 0
|
||||||
* @param data2 (const char*) folder name.
|
* @param data2 (const char*) folder name.
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_IMAP_FOLDER_EMPTIED 106
|
#define DC_EVENT_IMAP_FOLDER_EMPTIED 106
|
||||||
|
|
||||||
@@ -3964,7 +3959,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data1 0
|
* @param data1 0
|
||||||
* @param data2 (const char*) path name
|
* @param data2 (const char*) path name
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_NEW_BLOB_FILE 150
|
#define DC_EVENT_NEW_BLOB_FILE 150
|
||||||
|
|
||||||
@@ -3974,7 +3968,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data1 0
|
* @param data1 0
|
||||||
* @param data2 (const char*) path name
|
* @param data2 (const char*) path name
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_DELETED_BLOB_FILE 151
|
#define DC_EVENT_DELETED_BLOB_FILE 151
|
||||||
|
|
||||||
@@ -3987,7 +3980,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data1 0
|
* @param data1 0
|
||||||
* @param data2 (const char*) Warning string in english language.
|
* @param data2 (const char*) Warning string in english language.
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_WARNING 300
|
#define DC_EVENT_WARNING 300
|
||||||
|
|
||||||
@@ -4010,7 +4002,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* Some error strings are taken from dc_set_stock_translation(),
|
* Some error strings are taken from dc_set_stock_translation(),
|
||||||
* however, most error strings will be in english language.
|
* however, most error strings will be in english language.
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_ERROR 400
|
#define DC_EVENT_ERROR 400
|
||||||
|
|
||||||
@@ -4034,7 +4025,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* 0=subsequent network error, should be logged only
|
* 0=subsequent network error, should be logged only
|
||||||
* @param data2 (const char*) Error string, always set, never NULL.
|
* @param data2 (const char*) Error string, always set, never NULL.
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_ERROR_NETWORK 401
|
#define DC_EVENT_ERROR_NETWORK 401
|
||||||
|
|
||||||
@@ -4050,7 +4040,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data2 (const char*) Info string in english language.
|
* @param data2 (const char*) Info string in english language.
|
||||||
* Must not be unref'd or modified
|
* Must not be unref'd or modified
|
||||||
* and is valid only until the callback returns.
|
* and is valid only until the callback returns.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_ERROR_SELF_NOT_IN_GROUP 410
|
#define DC_EVENT_ERROR_SELF_NOT_IN_GROUP 410
|
||||||
|
|
||||||
@@ -4064,7 +4053,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
*
|
*
|
||||||
* @param data1 (int) chat_id for single added messages
|
* @param data1 (int) chat_id for single added messages
|
||||||
* @param data2 (int) msg_id for single added messages
|
* @param data2 (int) msg_id for single added messages
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_MSGS_CHANGED 2000
|
#define DC_EVENT_MSGS_CHANGED 2000
|
||||||
|
|
||||||
@@ -4077,7 +4065,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
*
|
*
|
||||||
* @param data1 (int) chat_id
|
* @param data1 (int) chat_id
|
||||||
* @param data2 (int) msg_id
|
* @param data2 (int) msg_id
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_INCOMING_MSG 2005
|
#define DC_EVENT_INCOMING_MSG 2005
|
||||||
|
|
||||||
@@ -4088,7 +4075,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
*
|
*
|
||||||
* @param data1 (int) chat_id
|
* @param data1 (int) chat_id
|
||||||
* @param data2 (int) msg_id
|
* @param data2 (int) msg_id
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_MSG_DELIVERED 2010
|
#define DC_EVENT_MSG_DELIVERED 2010
|
||||||
|
|
||||||
@@ -4099,7 +4085,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
*
|
*
|
||||||
* @param data1 (int) chat_id
|
* @param data1 (int) chat_id
|
||||||
* @param data2 (int) msg_id
|
* @param data2 (int) msg_id
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_MSG_FAILED 2012
|
#define DC_EVENT_MSG_FAILED 2012
|
||||||
|
|
||||||
@@ -4110,7 +4095,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
*
|
*
|
||||||
* @param data1 (int) chat_id
|
* @param data1 (int) chat_id
|
||||||
* @param data2 (int) msg_id
|
* @param data2 (int) msg_id
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_MSG_READ 2015
|
#define DC_EVENT_MSG_READ 2015
|
||||||
|
|
||||||
@@ -4123,7 +4107,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
*
|
*
|
||||||
* @param data1 (int) chat_id
|
* @param data1 (int) chat_id
|
||||||
* @param data2 0
|
* @param data2 0
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_CHAT_MODIFIED 2020
|
#define DC_EVENT_CHAT_MODIFIED 2020
|
||||||
|
|
||||||
@@ -4133,7 +4116,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
*
|
*
|
||||||
* @param data1 (int) If not 0, this is the contact_id of an added contact that should be selected.
|
* @param data1 (int) If not 0, this is the contact_id of an added contact that should be selected.
|
||||||
* @param data2 0
|
* @param data2 0
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_CONTACTS_CHANGED 2030
|
#define DC_EVENT_CONTACTS_CHANGED 2030
|
||||||
|
|
||||||
@@ -4146,7 +4128,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* If the locations of several contacts have been changed,
|
* If the locations of several contacts have been changed,
|
||||||
* eg. after calling dc_delete_all_locations(), this parameter is set to 0.
|
* eg. after calling dc_delete_all_locations(), this parameter is set to 0.
|
||||||
* @param data2 0
|
* @param data2 0
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_LOCATION_CHANGED 2035
|
#define DC_EVENT_LOCATION_CHANGED 2035
|
||||||
|
|
||||||
@@ -4156,7 +4137,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
*
|
*
|
||||||
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
|
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
* @param data2 0
|
* @param data2 0
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_CONFIGURE_PROGRESS 2041
|
#define DC_EVENT_CONFIGURE_PROGRESS 2041
|
||||||
|
|
||||||
@@ -4166,7 +4146,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
*
|
*
|
||||||
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
|
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
* @param data2 0
|
* @param data2 0
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_IMEX_PROGRESS 2051
|
#define DC_EVENT_IMEX_PROGRESS 2051
|
||||||
|
|
||||||
@@ -4181,7 +4160,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data1 (const char*) Path and file name.
|
* @param data1 (const char*) Path and file name.
|
||||||
* Must not be unref'd or modified and is valid only until the callback returns.
|
* Must not be unref'd or modified and is valid only until the callback returns.
|
||||||
* @param data2 0
|
* @param data2 0
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_IMEX_FILE_WRITTEN 2052
|
#define DC_EVENT_IMEX_FILE_WRITTEN 2052
|
||||||
|
|
||||||
@@ -4199,7 +4177,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||||
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||||
* 1000=Protocol finished for this contact.
|
* 1000=Protocol finished for this contact.
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060
|
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060
|
||||||
|
|
||||||
@@ -4215,21 +4192,9 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* @param data2 (int) Progress as:
|
* @param data2 (int) Progress as:
|
||||||
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||||
* (Bob has verified alice and waits until Alice does the same for him)
|
* (Bob has verified alice and waits until Alice does the same for him)
|
||||||
* @return 0
|
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
|
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This event is sent out to the inviter when a joiner successfully joined a group.
|
|
||||||
*
|
|
||||||
* @param data1 (int) chat_id
|
|
||||||
* @param data2 (int) contact_id
|
|
||||||
* @return 0
|
|
||||||
*/
|
|
||||||
#define DC_EVENT_SECUREJOIN_MEMBER_ADDED 2062
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
*/
|
*/
|
||||||
@@ -4245,8 +4210,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
#define DC_EVENT_DATA2_IS_STRING(e) ((e)>=100 && (e)<=499)
|
#define DC_EVENT_DATA2_IS_STRING(e) ((e)>=100 && (e)<=499)
|
||||||
#define DC_EVENT_RETURNS_INT(e) ((e)==DC_EVENT_IS_OFFLINE) // not used anymore
|
#define DC_EVENT_RETURNS_INT(e) ((e)==DC_EVENT_IS_OFFLINE) // not used anymore
|
||||||
#define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore
|
#define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore
|
||||||
char* dc_get_version_str (void); // deprecated
|
|
||||||
void dc_array_add_id (dc_array_t*, uint32_t); // deprecated
|
|
||||||
#define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore
|
#define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore
|
||||||
#define dc_chat_get_archived(a) (dc_chat_get_visibility((a))==1? 1 : 0) // not used anymore
|
#define dc_chat_get_archived(a) (dc_chat_get_visibility((a))==1? 1 : 0) // not used anymore
|
||||||
|
|
||||||
@@ -4258,6 +4221,14 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
|
|||||||
#define DC_SHOW_EMAILS_ACCEPTED_CONTACTS 1
|
#define DC_SHOW_EMAILS_ACCEPTED_CONTACTS 1
|
||||||
#define DC_SHOW_EMAILS_ALL 2
|
#define DC_SHOW_EMAILS_ALL 2
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Values for dc_get|set_config("media_quality")
|
||||||
|
*/
|
||||||
|
#define DC_MEDIA_QUALITY_BALANCED 0
|
||||||
|
#define DC_MEDIA_QUALITY_WORSE 1
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Values for dc_get|set_config("key_gen_type")
|
* Values for dc_get|set_config("key_gen_type")
|
||||||
*/
|
*/
|
||||||
@@ -4376,8 +4347,6 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
|
|||||||
#define DC_STR_NOMESSAGES 1
|
#define DC_STR_NOMESSAGES 1
|
||||||
#define DC_STR_SELF 2
|
#define DC_STR_SELF 2
|
||||||
#define DC_STR_DRAFT 3
|
#define DC_STR_DRAFT 3
|
||||||
#define DC_STR_MEMBER 4
|
|
||||||
#define DC_STR_CONTACT 6
|
|
||||||
#define DC_STR_VOICEMESSAGE 7
|
#define DC_STR_VOICEMESSAGE 7
|
||||||
#define DC_STR_DEADDROP 8
|
#define DC_STR_DEADDROP 8
|
||||||
#define DC_STR_IMAGE 9
|
#define DC_STR_IMAGE 9
|
||||||
@@ -4409,7 +4378,6 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
|
|||||||
#define DC_STR_STARREDMSGS 41
|
#define DC_STR_STARREDMSGS 41
|
||||||
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
|
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
|
||||||
#define DC_STR_AC_SETUP_MSG_BODY 43
|
#define DC_STR_AC_SETUP_MSG_BODY 43
|
||||||
#define DC_STR_SELFTALK_SUBTITLE 50
|
|
||||||
#define DC_STR_CANNOT_LOGIN 60
|
#define DC_STR_CANNOT_LOGIN 60
|
||||||
#define DC_STR_SERVER_RESPONSE 61
|
#define DC_STR_SERVER_RESPONSE 61
|
||||||
#define DC_STR_MSGACTIONBYUSER 62
|
#define DC_STR_MSGACTIONBYUSER 62
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ extern crate human_panic;
|
|||||||
extern crate num_traits;
|
extern crate num_traits;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::BTreeMap;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
@@ -22,13 +22,14 @@ use std::str::FromStr;
|
|||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use async_std::task::block_on;
|
use async_std::task::block_on;
|
||||||
use libc::uintptr_t;
|
use libc::uintptr_t;
|
||||||
use num_traits::{FromPrimitive, ToPrimitive};
|
use num_traits::{FromPrimitive, ToPrimitive};
|
||||||
|
|
||||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
|
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
|
||||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||||
use deltachat::contact::Contact;
|
use deltachat::contact::{Contact, Origin};
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::key::DcKey;
|
use deltachat::key::DcKey;
|
||||||
use deltachat::message::MsgId;
|
use deltachat::message::MsgId;
|
||||||
@@ -177,7 +178,7 @@ macro_rules! try_inner_async {
|
|||||||
let $name = ctx;
|
let $name = ctx;
|
||||||
$block.await
|
$block.await
|
||||||
}
|
}
|
||||||
None => Err(failure::err_msg("context not open")),
|
None => Err(anyhow!("context not open")),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}};
|
}};
|
||||||
@@ -416,7 +417,7 @@ pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_info(
|
fn render_info(
|
||||||
info: HashMap<&'static str, String>,
|
info: BTreeMap<&'static str, String>,
|
||||||
) -> std::result::Result<String, std::fmt::Error> {
|
) -> std::result::Result<String, std::fmt::Error> {
|
||||||
let mut res = String::new();
|
let mut res = String::new();
|
||||||
for (key, value) in &info {
|
for (key, value) in &info {
|
||||||
@@ -449,11 +450,6 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
|
|||||||
.unwrap_or_else(|_| ptr::null_mut())
|
.unwrap_or_else(|_| ptr::null_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_get_version_str() -> *mut libc::c_char {
|
|
||||||
context::get_version_str().strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
|
pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
|
||||||
if context.is_null() {
|
if context.is_null() {
|
||||||
@@ -656,14 +652,6 @@ unsafe fn translate_event(event: Event) -> *mut dc_event_t {
|
|||||||
data1: contact_id as uintptr_t,
|
data1: contact_id as uintptr_t,
|
||||||
data2: progress as uintptr_t,
|
data2: progress as uintptr_t,
|
||||||
},
|
},
|
||||||
Event::SecurejoinMemberAdded {
|
|
||||||
chat_id,
|
|
||||||
contact_id,
|
|
||||||
} => EventWrapper {
|
|
||||||
event_id,
|
|
||||||
data1: chat_id.to_u32() as uintptr_t,
|
|
||||||
data2: contact_id as uintptr_t,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Box::into_raw(Box::new(wrapper))
|
Box::into_raw(Box::new(wrapper))
|
||||||
@@ -1072,12 +1060,12 @@ pub unsafe extern "C" fn dc_estimate_deletion_cnt(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let ffi_context = &*context;
|
let ffi_context = &*context;
|
||||||
ffi_context
|
with_inner_async!(ffi_context, ctx, async move {
|
||||||
.with_inner(|ctx| {
|
message::estimate_deletion_cnt(ctx, from_server != 0, seconds)
|
||||||
message::estimate_deletion_cnt(ctx, from_server != 0, seconds).unwrap_or(0)
|
.await
|
||||||
as libc::c_int
|
.unwrap_or(0) as libc::c_int
|
||||||
})
|
})
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -1700,7 +1688,7 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
|
|||||||
with_inner_async!(
|
with_inner_async!(
|
||||||
ffi_context,
|
ffi_context,
|
||||||
ctx,
|
ctx,
|
||||||
Contact::lookup_id_by_addr(&ctx, to_string_lossy(addr))
|
Contact::lookup_id_by_addr(&ctx, to_string_lossy(addr), Origin::IncomingReplyTo)
|
||||||
)
|
)
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
@@ -2171,16 +2159,6 @@ pub unsafe extern "C" fn dc_array_unref(a: *mut dc_array::dc_array_t) {
|
|||||||
Box::from_raw(a);
|
Box::from_raw(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_array_add_id(array: *mut dc_array_t, item: libc::c_uint) {
|
|
||||||
if array.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_array_add_id()");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*array).add_id(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_array_get_cnt(array: *const dc_array_t) -> libc::size_t {
|
pub unsafe extern "C" fn dc_array_get_cnt(array: *const dc_array_t) -> libc::size_t {
|
||||||
if array.is_null() {
|
if array.is_null() {
|
||||||
@@ -2511,20 +2489,6 @@ pub unsafe extern "C" fn dc_chat_get_name(chat: *mut dc_chat_t) -> *mut libc::c_
|
|||||||
ffi_chat.chat.get_name().strdup()
|
ffi_chat.chat.get_name().strdup()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_chat_get_subtitle(chat: *mut dc_chat_t) -> *mut libc::c_char {
|
|
||||||
if chat.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_chat_get_subtitle()");
|
|
||||||
return "".strdup();
|
|
||||||
}
|
|
||||||
let ffi_chat = &*chat;
|
|
||||||
let ffi_context: &ContextWrapper = &*ffi_chat.context;
|
|
||||||
|
|
||||||
with_inner_async!(ffi_context, ctx, ffi_chat.chat.get_subtitle(&ctx))
|
|
||||||
.map(|s| s.strdup())
|
|
||||||
.unwrap_or_else(|_| "".strdup())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut libc::c_char {
|
pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut libc::c_char {
|
||||||
if chat.is_null() {
|
if chat.is_null() {
|
||||||
@@ -2848,7 +2812,7 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
|
|||||||
ffi_msg
|
ffi_msg
|
||||||
.message
|
.message
|
||||||
.get_file(ctx)
|
.get_file(ctx)
|
||||||
.map(|p| p.strdup())
|
.map(|p| p.to_string_lossy().strdup())
|
||||||
.unwrap_or_else(|| "".strdup())
|
.unwrap_or_else(|| "".strdup())
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|_| "".strdup())
|
.unwrap_or_else(|_| "".strdup())
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use failure::Fail;
|
|
||||||
use std::ffi::{CStr, CString};
|
use std::ffi::{CStr, CString};
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
|
|
||||||
@@ -31,13 +30,13 @@ unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Error type for the [OsStrExt] trait
|
/// Error type for the [OsStrExt] trait
|
||||||
#[derive(Debug, Fail, PartialEq)]
|
#[derive(Debug, PartialEq, thiserror::Error)]
|
||||||
pub(crate) enum CStringError {
|
pub(crate) enum CStringError {
|
||||||
/// The string contains an interior null byte
|
/// The string contains an interior null byte
|
||||||
#[fail(display = "String contains an interior null byte")]
|
#[error("String contains an interior null byte")]
|
||||||
InteriorNullByte,
|
InteriorNullByte,
|
||||||
/// The string is not valid Unicode
|
/// The string is not valid Unicode
|
||||||
#[fail(display = "String is not valid unicode")]
|
#[error("String is not valid unicode")]
|
||||||
NotUnicode,
|
NotUnicode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ extern crate proc_macro;
|
|||||||
|
|
||||||
use crate::proc_macro::TokenStream;
|
use crate::proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn;
|
|
||||||
|
|
||||||
// For now, assume (not check) that these macroses are applied to enum without
|
// For now, assume (not check) that these macroses are applied to enum without
|
||||||
// data. If this assumption is violated, compiler error will point to
|
// data. If this assumption is violated, compiler error will point to
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::{bail, ensure};
|
||||||
use async_std::path::Path;
|
use async_std::path::Path;
|
||||||
|
|
||||||
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
|
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
|
||||||
use deltachat::chatlist::*;
|
use deltachat::chatlist::*;
|
||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
@@ -92,7 +92,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), Error> {
|
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), anyhow::Error> {
|
||||||
let data = dc_read_file(context, filename).await?;
|
let data = dc_read_file(context, filename).await?;
|
||||||
|
|
||||||
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false).await {
|
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false).await {
|
||||||
@@ -292,11 +292,7 @@ fn chat_prefix(chat: &Chat) -> &'static str {
|
|||||||
chat.typ.into()
|
chat.typ.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cmdline(
|
pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Result<(), Error> {
|
||||||
context: Context,
|
|
||||||
line: &str,
|
|
||||||
chat_id: &mut ChatId,
|
|
||||||
) -> Result<(), failure::Error> {
|
|
||||||
let mut sel_chat = if !chat_id.is_unset() {
|
let mut sel_chat = if !chat_id.is_unset() {
|
||||||
Chat::load_from_db(&context, *chat_id).await.ok()
|
Chat::load_from_db(&context, *chat_id).await.ok()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,13 +7,16 @@
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate deltachat;
|
extern crate deltachat;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate failure;
|
extern crate lazy_static;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate rusqlite;
|
||||||
|
|
||||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use ansi_term::Color;
|
use ansi_term::Color;
|
||||||
|
use anyhow::{bail, Error};
|
||||||
use async_std::path::Path;
|
use async_std::path::Path;
|
||||||
use deltachat::chat::ChatId;
|
use deltachat::chat::ChatId;
|
||||||
use deltachat::config;
|
use deltachat::config;
|
||||||
@@ -269,10 +272,10 @@ impl Highlighter for DcHelper {
|
|||||||
|
|
||||||
impl Helper for DcHelper {}
|
impl Helper for DcHelper {}
|
||||||
|
|
||||||
async fn start(args: Vec<String>) -> Result<(), failure::Error> {
|
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
println!("Error: Bad arguments, expected [db-name].");
|
println!("Error: Bad arguments, expected [db-name].");
|
||||||
return Err(format_err!("No db-name specified"));
|
bail!("No db-name specified");
|
||||||
}
|
}
|
||||||
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf()).await?;
|
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf()).await?;
|
||||||
|
|
||||||
@@ -353,7 +356,7 @@ async fn handle_cmd(
|
|||||||
line: &str,
|
line: &str,
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
selected_chat: &mut ChatId,
|
selected_chat: &mut ChatId,
|
||||||
) -> Result<ExitResult, failure::Error> {
|
) -> Result<ExitResult, Error> {
|
||||||
let mut args = line.splitn(2, ' ');
|
let mut args = line.splitn(2, ' ');
|
||||||
let arg0 = args.next().unwrap_or_default();
|
let arg0 = args.next().unwrap_or_default();
|
||||||
let arg1 = args.next().unwrap_or_default();
|
let arg1 = args.next().unwrap_or_default();
|
||||||
@@ -417,7 +420,7 @@ async fn handle_cmd(
|
|||||||
Ok(ExitResult::Continue)
|
Ok(ExitResult::Continue)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), failure::Error> {
|
fn main() -> Result<(), Error> {
|
||||||
let _ = pretty_env_logger::try_init();
|
let _ = pretty_env_logger::try_init();
|
||||||
|
|
||||||
let args = std::env::args().collect();
|
let args = std::env::args().collect();
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
0.900.0 (DRAFT)
|
||||||
|
---------------
|
||||||
|
|
||||||
|
- refactored internals to use plugin-approach
|
||||||
|
|
||||||
|
- introduced PerAccount and Global hooks that plugins can implement
|
||||||
|
|
||||||
|
- introduced `ac_member_added()` and `ac_member_removed()` plugin events.
|
||||||
|
|
||||||
|
- introduced two documented examples for an echo and a group-membership
|
||||||
|
tracking plugin.
|
||||||
|
|
||||||
0.800.0
|
0.800.0
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
high level API reference
|
high level API reference
|
||||||
========================
|
========================
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This API is work in progress and may change in versions prior to 1.0.
|
|
||||||
|
|
||||||
- :class:`deltachat.account.Account` (your main entry point, creates the
|
- :class:`deltachat.account.Account` (your main entry point, creates the
|
||||||
other classes)
|
other classes)
|
||||||
- :class:`deltachat.contact.Contact`
|
- :class:`deltachat.contact.Contact`
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
C deltachat interface
|
|
||||||
=====================
|
|
||||||
|
|
||||||
See :doc:`lapi` for accessing many of the below functions
|
|
||||||
through the ``deltachat.capi.lib`` namespace.
|
|
||||||
|
|
||||||
@@ -1,37 +1,60 @@
|
|||||||
|
|
||||||
|
|
||||||
examples
|
examples
|
||||||
========
|
========
|
||||||
|
|
||||||
|
|
||||||
Playing around on the commandline
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
Once you have :doc:`installed deltachat bindings <install>`
|
Once you have :doc:`installed deltachat bindings <install>`
|
||||||
you can start playing from the python interpreter commandline.
|
you need email/password credentials for an IMAP/SMTP account.
|
||||||
For example you can type ``python`` and then::
|
Delta Chat developers and the CI system use a special URL to create
|
||||||
|
temporary e-mail accounts on [testrun.org](https://testrun.org) for testing.
|
||||||
|
|
||||||
# instantiate and configure deltachat account
|
Receiving a Chat message from the command line
|
||||||
import deltachat
|
----------------------------------------------
|
||||||
ac = deltachat.Account("/tmp/db")
|
|
||||||
|
|
||||||
# start configuration activity and smtp/imap threads
|
Here is a simple bot that:
|
||||||
ac.start_threads()
|
|
||||||
ac.configure(addr="test2@hq5.merlinux.eu", mail_pw="********")
|
|
||||||
|
|
||||||
# create a contact and send a message
|
- receives a message and sends back ("echoes") a message
|
||||||
contact = ac.create_contact("someother@email.address")
|
|
||||||
chat = ac.create_chat_by_contact(contact)
|
|
||||||
chat.send_text("hi from the python interpreter command line")
|
|
||||||
|
|
||||||
Checkout our :doc:`api` for the various high-level things you can do
|
- terminates the bot if the message `/quit` is sent
|
||||||
to send/receive messages, create contacts and chats.
|
|
||||||
|
|
||||||
|
.. include:: ../examples/echo_and_quit.py
|
||||||
|
:literal:
|
||||||
|
|
||||||
Looking at a real example
|
With this file in your working directory you can run the bot
|
||||||
|
by specifying a database path, an e-mail address and password of
|
||||||
|
a SMTP-IMAP account::
|
||||||
|
|
||||||
|
$ cd examples
|
||||||
|
$ python echo_and_quit.py /tmp/db --email ADDRESS --password PASSWORD
|
||||||
|
|
||||||
|
While this process is running you can start sending chat messages
|
||||||
|
to `ADDRESS`.
|
||||||
|
|
||||||
|
Track member additions and removals in a group
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
Here is a simple bot that:
|
||||||
|
|
||||||
|
- echoes messages sent to it
|
||||||
|
|
||||||
|
- tracks if configuration completed
|
||||||
|
|
||||||
|
- tracks member additions and removals for all chat groups
|
||||||
|
|
||||||
|
.. include:: ../examples/group_tracking.py
|
||||||
|
:literal:
|
||||||
|
|
||||||
|
With this file in your working directory you can run the bot
|
||||||
|
by specifying a database path, an e-mail address and password of
|
||||||
|
a SMTP-IMAP account::
|
||||||
|
|
||||||
|
python group_tracking.py --email ADDRESS --password PASSWORD /tmp/db
|
||||||
|
|
||||||
|
When this process is running you can start sending chat messages
|
||||||
|
to `ADDRESS`.
|
||||||
|
|
||||||
|
Writing bots for real
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
The `deltabot repository <https://github.com/deltachat/deltabot#deltachat-example-bot>`_
|
The `deltabot repository <https://github.com/deltachat/deltabot#deltachat-example-bot>`_
|
||||||
contains a real-life example of Python bindings usage.
|
contains a little framework for writing deltachat bots in Python.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ deltachat python bindings
|
|||||||
The ``deltachat`` Python package provides two layers of bindings for the
|
The ``deltachat`` Python package provides two layers of bindings for the
|
||||||
core Rust-library of the https://delta.chat messaging ecosystem:
|
core Rust-library of the https://delta.chat messaging ecosystem:
|
||||||
|
|
||||||
- :doc:`api` is a high level interface to deltachat-core which aims
|
- :doc:`api` is a high level interface to deltachat-core.
|
||||||
to be memory safe and thoroughly tested through continous tox/pytest runs.
|
|
||||||
|
- :doc:`plugins` is a brief introduction into implementing plugin hooks.
|
||||||
|
|
||||||
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
|
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
|
||||||
<https://github.com/deltachat/deltachat-core-rust>`_.
|
<https://github.com/deltachat/deltachat-core-rust>`_.
|
||||||
@@ -28,6 +29,7 @@ getting started
|
|||||||
changelog
|
changelog
|
||||||
api
|
api
|
||||||
lapi
|
lapi
|
||||||
|
plugins
|
||||||
|
|
||||||
..
|
..
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|||||||
38
python/doc/plugins.rst
Normal file
38
python/doc/plugins.rst
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
Implementing Plugin Hooks
|
||||||
|
==========================
|
||||||
|
|
||||||
|
The Delta Chat Python bindings use `pluggy <https://pluggy.readthedocs.io>`_
|
||||||
|
for managing global and per-account plugin registration, and performing
|
||||||
|
hook calls. There are two kinds of plugins:
|
||||||
|
|
||||||
|
- Global plugins that are active for all accounts; they can implement
|
||||||
|
hooks at account-creation and account-shutdown time.
|
||||||
|
|
||||||
|
- Account plugins that are only active during the lifetime of a
|
||||||
|
single Account instance.
|
||||||
|
|
||||||
|
|
||||||
|
Registering a plugin
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. autofunction:: deltachat.register_global_plugin
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
.. automethod:: deltachat.account.Account.add_account_plugin
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
|
||||||
|
Per-Account Hook specifications
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.hookspec.PerAccount
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
Global Hook specifications
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.hookspec.Global
|
||||||
|
:members:
|
||||||
|
|
||||||
30
python/examples/echo_and_quit.py
Normal file
30
python/examples/echo_and_quit.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
# content of echo_and_quit.py
|
||||||
|
|
||||||
|
from deltachat import account_hookimpl, run_cmdline
|
||||||
|
|
||||||
|
|
||||||
|
class EchoPlugin:
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_incoming_message(self, message):
|
||||||
|
print("process_incoming message", message)
|
||||||
|
if message.text.strip() == "/quit":
|
||||||
|
message.account.shutdown()
|
||||||
|
else:
|
||||||
|
# unconditionally accept the chat
|
||||||
|
message.accept_sender_contact()
|
||||||
|
addr = message.get_sender_contact().addr
|
||||||
|
text = message.text
|
||||||
|
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_message_delivered(self, message):
|
||||||
|
print("ac_message_delivered", message)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
run_cmdline(argv=argv, account_plugins=[EchoPlugin()])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
52
python/examples/group_tracking.py
Normal file
52
python/examples/group_tracking.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
# content of group_tracking.py
|
||||||
|
|
||||||
|
from deltachat import account_hookimpl, run_cmdline
|
||||||
|
|
||||||
|
|
||||||
|
class GroupTrackingPlugin:
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_incoming_message(self, message):
|
||||||
|
print("process_incoming message", message)
|
||||||
|
if message.text.strip() == "/quit":
|
||||||
|
message.account.shutdown()
|
||||||
|
else:
|
||||||
|
# unconditionally accept the chat
|
||||||
|
message.accept_sender_contact()
|
||||||
|
addr = message.get_sender_contact().addr
|
||||||
|
text = message.text
|
||||||
|
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_outgoing_message(self, message):
|
||||||
|
print("ac_outgoing_message:", message)
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_configure_completed(self, success):
|
||||||
|
print("ac_configure_completed:", success)
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_chat_modified(self, chat):
|
||||||
|
print("ac_chat_modified:", chat.id, chat.get_name())
|
||||||
|
for member in chat.get_contacts():
|
||||||
|
print("chat member: {}".format(member.addr))
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_member_added(self, chat, contact, message):
|
||||||
|
print("ac_member_added {} to chat {} from {}".format(
|
||||||
|
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||||
|
for member in chat.get_contacts():
|
||||||
|
print("chat member: {}".format(member.addr))
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_member_removed(self, chat, contact, message):
|
||||||
|
print("ac_member_removed {} from chat {} by {}".format(
|
||||||
|
contact.addr, chat.id, message.get_sender_contact().addr))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
72
python/examples/test_examples.py
Normal file
72
python/examples/test_examples.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
|
||||||
|
import pytest
|
||||||
|
import py
|
||||||
|
import echo_and_quit
|
||||||
|
import group_tracking
|
||||||
|
from deltachat.eventlogger import FFIEventLogger
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def datadir():
|
||||||
|
"""The py.path.local object of the test-data/ directory."""
|
||||||
|
for path in reversed(py.path.local(__file__).parts()):
|
||||||
|
datadir = path.join('test-data')
|
||||||
|
if datadir.isdir():
|
||||||
|
return datadir
|
||||||
|
else:
|
||||||
|
pytest.skip('test-data directory not found')
|
||||||
|
|
||||||
|
|
||||||
|
def test_echo_quit_plugin(acfactory):
|
||||||
|
botproc = acfactory.run_bot_process(echo_and_quit)
|
||||||
|
|
||||||
|
ac1 = acfactory.get_one_online_account()
|
||||||
|
bot_contact = ac1.create_contact(botproc.addr)
|
||||||
|
ch1 = ac1.create_chat_by_contact(bot_contact)
|
||||||
|
ch1.send_text("hello")
|
||||||
|
reply = ac1._evtracker.wait_next_incoming_message()
|
||||||
|
assert "hello" in reply.text
|
||||||
|
assert reply.chat == ch1
|
||||||
|
ch1.send_text("/quit")
|
||||||
|
botproc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
botproc.fnmatch_lines("""
|
||||||
|
*ac_configure_completed*
|
||||||
|
""")
|
||||||
|
ac1.add_account_plugin(FFIEventLogger(ac1, "ac1"))
|
||||||
|
ac2.add_account_plugin(FFIEventLogger(ac2, "ac2"))
|
||||||
|
|
||||||
|
lp.sec("creating bot test group with bot")
|
||||||
|
bot_contact = ac1.create_contact(botproc.addr)
|
||||||
|
ch = ac1.create_group_chat("bot test group")
|
||||||
|
ch.add_contact(bot_contact)
|
||||||
|
ch.send_text("hello")
|
||||||
|
|
||||||
|
botproc.fnmatch_lines("""
|
||||||
|
*ac_chat_modified*bot test group*
|
||||||
|
""")
|
||||||
|
|
||||||
|
lp.sec("adding third member {}".format(ac2.get_config("addr")))
|
||||||
|
contact3 = ac1.create_contact(ac2.get_config("addr"))
|
||||||
|
ch.add_contact(contact3)
|
||||||
|
|
||||||
|
reply = ac1._evtracker.wait_next_incoming_message()
|
||||||
|
assert "hello" in reply.text
|
||||||
|
|
||||||
|
lp.sec("now looking at what the bot received")
|
||||||
|
botproc.fnmatch_lines("""
|
||||||
|
*ac_member_added {}*
|
||||||
|
""".format(contact3.addr))
|
||||||
|
|
||||||
|
lp.sec("contact successfully added, now removing")
|
||||||
|
ch.remove_contact(contact3)
|
||||||
|
botproc.fnmatch_lines("""
|
||||||
|
*ac_member_removed {}*
|
||||||
|
""".format(contact3.addr))
|
||||||
@@ -22,6 +22,11 @@ def main():
|
|||||||
packages=setuptools.find_packages('src'),
|
packages=setuptools.find_packages('src'),
|
||||||
package_dir={'': 'src'},
|
package_dir={'': 'src'},
|
||||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||||
|
entry_points = {
|
||||||
|
'pytest11': [
|
||||||
|
'deltachat.testplugin = deltachat.testplugin',
|
||||||
|
],
|
||||||
|
},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 4 - Beta',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
from deltachat import capi, const
|
import sys
|
||||||
from deltachat.capi import ffi
|
|
||||||
from deltachat.account import Account # noqa
|
from . import capi, const, hookspec
|
||||||
|
from .capi import ffi
|
||||||
|
from .account import Account # noqa
|
||||||
|
from .message import Message # noqa
|
||||||
|
from .contact import Contact # noqa
|
||||||
|
from .chat import Chat # noqa
|
||||||
|
from .hookspec import account_hookimpl, global_hookimpl # noqa
|
||||||
|
from . import eventlogger
|
||||||
|
|
||||||
from pkg_resources import get_distribution, DistributionNotFound
|
from pkg_resources import get_distribution, DistributionNotFound
|
||||||
try:
|
try:
|
||||||
@@ -64,3 +71,62 @@ def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
|||||||
if name.startswith("DC_EVENT_"):
|
if name.startswith("DC_EVENT_"):
|
||||||
_DC_EVENTNAME_MAP[val] = name
|
_DC_EVENTNAME_MAP[val] = name
|
||||||
return _DC_EVENTNAME_MAP[integer]
|
return _DC_EVENTNAME_MAP[integer]
|
||||||
|
|
||||||
|
|
||||||
|
def register_global_plugin(plugin):
|
||||||
|
""" Register a global plugin which implements one or more
|
||||||
|
of the :class:`deltachat.hookspec.Global` hooks.
|
||||||
|
"""
|
||||||
|
gm = hookspec.Global._get_plugin_manager()
|
||||||
|
gm.register(plugin)
|
||||||
|
gm.check_pending()
|
||||||
|
|
||||||
|
|
||||||
|
def unregister_global_plugin(plugin):
|
||||||
|
gm = hookspec.Global._get_plugin_manager()
|
||||||
|
gm.unregister(plugin)
|
||||||
|
|
||||||
|
|
||||||
|
register_global_plugin(eventlogger)
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmdline(argv=None, account_plugins=None):
|
||||||
|
""" Run a simple default command line app, registering the specified
|
||||||
|
account plugins. """
|
||||||
|
import argparse
|
||||||
|
if argv is None:
|
||||||
|
argv = sys.argv
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(prog=argv[0] if argv else None)
|
||||||
|
parser.add_argument("db", action="store", help="database file")
|
||||||
|
parser.add_argument("--show-ffi", action="store_true", help="show low level ffi events")
|
||||||
|
parser.add_argument("--email", action="store", help="email address")
|
||||||
|
parser.add_argument("--password", action="store", help="password")
|
||||||
|
|
||||||
|
args = parser.parse_args(argv[1:])
|
||||||
|
|
||||||
|
ac = Account(args.db)
|
||||||
|
|
||||||
|
if args.show_ffi:
|
||||||
|
log = eventlogger.FFIEventLogger(ac, "bot")
|
||||||
|
ac.add_account_plugin(log)
|
||||||
|
|
||||||
|
if not ac.is_configured():
|
||||||
|
assert args.email and args.password, (
|
||||||
|
"you must specify --email and --password once to configure this database/account"
|
||||||
|
)
|
||||||
|
ac.set_config("addr", args.email)
|
||||||
|
ac.set_config("mail_pw", args.password)
|
||||||
|
ac.set_config("mvbox_move", "0")
|
||||||
|
ac.set_config("mvbox_watch", "0")
|
||||||
|
ac.set_config("sentbox_watch", "0")
|
||||||
|
|
||||||
|
for plugin in account_plugins or []:
|
||||||
|
ac.add_account_plugin(plugin)
|
||||||
|
|
||||||
|
# start IO threads and configure if neccessary
|
||||||
|
ac.start()
|
||||||
|
|
||||||
|
print("{}: waiting for message".format(ac.get_config("addr")))
|
||||||
|
|
||||||
|
ac.wait_shutdown()
|
||||||
|
|||||||
@@ -2,21 +2,25 @@
|
|||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import atexit
|
import atexit
|
||||||
import threading
|
from contextlib import contextmanager
|
||||||
|
from email.utils import parseaddr
|
||||||
|
import queue
|
||||||
|
from threading import Event
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
from array import array
|
from array import array
|
||||||
from queue import Queue
|
|
||||||
|
|
||||||
import deltachat
|
import deltachat
|
||||||
from . import const
|
from . import const
|
||||||
from .capi import ffi, lib
|
from .capi import ffi, lib
|
||||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
from .message import Message
|
from .message import Message, map_system_message
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
from .eventlogger import EventLogger
|
from .tracker import ImexTracker
|
||||||
from .hookspec import get_plugin_manager, hookimpl
|
from . import hookspec, iothreads
|
||||||
|
|
||||||
|
|
||||||
|
class MissingCredentials(ValueError):
|
||||||
|
""" Account is missing `addr` and `mail_pw` config values. """
|
||||||
|
|
||||||
|
|
||||||
class Account(object):
|
class Account(object):
|
||||||
@@ -24,45 +28,53 @@ class Account(object):
|
|||||||
by the underlying deltachat core library. All public Account methods are
|
by the underlying deltachat core library. All public Account methods are
|
||||||
meant to be memory-safe and return memory-safe objects.
|
meant to be memory-safe and return memory-safe objects.
|
||||||
"""
|
"""
|
||||||
def __init__(self, db_path, logid=None, os_name=None, debug=True):
|
MissingCredentials = MissingCredentials
|
||||||
|
|
||||||
|
def __init__(self, db_path, os_name=None):
|
||||||
""" initialize account object.
|
""" initialize account object.
|
||||||
|
|
||||||
:param db_path: a path to the account database. The database
|
:param db_path: a path to the account database. The database
|
||||||
will be created if it doesn't exist.
|
will be created if it doesn't exist.
|
||||||
:param logid: an optional logging prefix that should be used with
|
|
||||||
the default internal logging.
|
|
||||||
:param os_name: this will be put to the X-Mailer header in outgoing messages
|
:param os_name: this will be put to the X-Mailer header in outgoing messages
|
||||||
:param debug: turn on debug logging for events.
|
|
||||||
"""
|
"""
|
||||||
|
# initialize per-account plugin system
|
||||||
|
self._pm = hookspec.PerAccount._make_plugin_manager()
|
||||||
|
self.add_account_plugin(self)
|
||||||
|
|
||||||
self._dc_context = ffi.gc(
|
self._dc_context = ffi.gc(
|
||||||
lib.dc_context_new(ffi.NULL, as_dc_charpointer(os_name)),
|
lib.dc_context_new(ffi.NULL, as_dc_charpointer(os_name)),
|
||||||
_destroy_dc_context,
|
_destroy_dc_context,
|
||||||
)
|
)
|
||||||
self._evlogger = EventLogger(self, logid, debug)
|
|
||||||
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
|
|
||||||
|
|
||||||
# register event call back and initialize plugin system
|
hook = hookspec.Global._get_plugin_manager().hook
|
||||||
def _ll_event(ctx, evt_name, data1, data2):
|
|
||||||
assert ctx == self._dc_context
|
|
||||||
self.pluggy.hook.process_low_level_event(
|
|
||||||
account=self, event_name=evt_name, data1=data1, data2=data2
|
|
||||||
)
|
|
||||||
|
|
||||||
self.pluggy = get_plugin_manager()
|
self._threads = iothreads.IOThreads(self)
|
||||||
self.pluggy.register(self._evlogger)
|
self._hook_event_queue = queue.Queue()
|
||||||
deltachat.set_context_callback(self._dc_context, _ll_event)
|
self._in_use_iter_events = False
|
||||||
|
self._shutdown_event = Event()
|
||||||
|
|
||||||
# open database
|
# open database
|
||||||
|
self.db_path = db_path
|
||||||
if hasattr(db_path, "encode"):
|
if hasattr(db_path, "encode"):
|
||||||
db_path = db_path.encode("utf8")
|
db_path = db_path.encode("utf8")
|
||||||
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
||||||
raise ValueError("Could not dc_open: {}".format(db_path))
|
raise ValueError("Could not dc_open: {}".format(db_path))
|
||||||
self._configkeys = self.get_config("sys.config_keys").split()
|
self._configkeys = self.get_config("sys.config_keys").split()
|
||||||
atexit.register(self.shutdown)
|
atexit.register(self.shutdown)
|
||||||
|
hook.dc_account_init(account=self)
|
||||||
|
|
||||||
|
@hookspec.account_hookimpl
|
||||||
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
|
for name, kwargs in self._map_ffi_event(ffi_event):
|
||||||
|
ev = HookEvent(self, name=name, kwargs=kwargs)
|
||||||
|
self._hook_event_queue.put(ev)
|
||||||
|
|
||||||
# def __del__(self):
|
# def __del__(self):
|
||||||
# self.shutdown()
|
# self.shutdown()
|
||||||
|
|
||||||
|
def ac_log_line(self, msg):
|
||||||
|
self._pm.hook.ac_log_line(message=msg)
|
||||||
|
|
||||||
def _check_config_key(self, name):
|
def _check_config_key(self, name):
|
||||||
if name not in self._configkeys:
|
if name not in self._configkeys:
|
||||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||||
@@ -134,16 +146,15 @@ class Account(object):
|
|||||||
if res == 0:
|
if res == 0:
|
||||||
raise Exception("Failed to set key")
|
raise Exception("Failed to set key")
|
||||||
|
|
||||||
def configure(self, **kwargs):
|
def update_config(self, kwargs):
|
||||||
""" set config values and configure this account.
|
""" update config values.
|
||||||
|
|
||||||
:param kwargs: name=value config settings for this account.
|
:param kwargs: name=value config settings for this account.
|
||||||
values need to be unicode.
|
values need to be unicode.
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
for name, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
self.set_config(name, value)
|
self.set_config(key, str(value))
|
||||||
lib.dc_configure(self._dc_context)
|
|
||||||
|
|
||||||
def is_configured(self):
|
def is_configured(self):
|
||||||
""" determine if the account is configured already; an initial connection
|
""" determine if the account is configured already; an initial connection
|
||||||
@@ -151,7 +162,7 @@ class Account(object):
|
|||||||
|
|
||||||
:returns: True if account is configured.
|
:returns: True if account is configured.
|
||||||
"""
|
"""
|
||||||
return lib.dc_is_configured(self._dc_context)
|
return bool(lib.dc_is_configured(self._dc_context))
|
||||||
|
|
||||||
def set_avatar(self, img_path):
|
def set_avatar(self, img_path):
|
||||||
"""Set self avatar.
|
"""Set self avatar.
|
||||||
@@ -202,8 +213,7 @@ class Account(object):
|
|||||||
|
|
||||||
:returns: :class:`deltachat.contact.Contact`
|
:returns: :class:`deltachat.contact.Contact`
|
||||||
"""
|
"""
|
||||||
self.check_is_configured()
|
return Contact(self, const.DC_CONTACT_ID_SELF)
|
||||||
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
|
|
||||||
|
|
||||||
def create_contact(self, email, name=None):
|
def create_contact(self, email, name=None):
|
||||||
""" create a (new) Contact. If there already is a Contact
|
""" create a (new) Contact. If there already is a Contact
|
||||||
@@ -214,11 +224,14 @@ class Account(object):
|
|||||||
:param name: display name for this contact (optional)
|
:param name: display name for this contact (optional)
|
||||||
:returns: :class:`deltachat.contact.Contact` instance.
|
:returns: :class:`deltachat.contact.Contact` instance.
|
||||||
"""
|
"""
|
||||||
name = as_dc_charpointer(name)
|
realname, addr = parseaddr(email)
|
||||||
email = as_dc_charpointer(email)
|
if name:
|
||||||
contact_id = lib.dc_create_contact(self._dc_context, name, email)
|
realname = name
|
||||||
|
realname = as_dc_charpointer(realname)
|
||||||
|
addr = as_dc_charpointer(addr)
|
||||||
|
contact_id = lib.dc_create_contact(self._dc_context, realname, addr)
|
||||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
return Contact(self._dc_context, contact_id)
|
return Contact(self, contact_id)
|
||||||
|
|
||||||
def delete_contact(self, contact):
|
def delete_contact(self, contact):
|
||||||
""" delete a Contact.
|
""" delete a Contact.
|
||||||
@@ -231,6 +244,14 @@ class Account(object):
|
|||||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||||
|
|
||||||
|
def get_contact_by_addr(self, email):
|
||||||
|
""" get a contact for the email address or None if it's blocked or doesn't exist. """
|
||||||
|
_, addr = parseaddr(email)
|
||||||
|
addr = as_dc_charpointer(addr)
|
||||||
|
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||||
|
if contact_id:
|
||||||
|
return self.get_contact_by_id(contact_id)
|
||||||
|
|
||||||
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
||||||
""" get a (filtered) list of contacts.
|
""" get a (filtered) list of contacts.
|
||||||
|
|
||||||
@@ -250,7 +271,15 @@ class Account(object):
|
|||||||
lib.dc_get_contacts(self._dc_context, flags, query),
|
lib.dc_get_contacts(self._dc_context, flags, query),
|
||||||
lib.dc_array_unref
|
lib.dc_array_unref
|
||||||
)
|
)
|
||||||
return list(iter_array(dc_array, lambda x: Contact(self._dc_context, x)))
|
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||||
|
|
||||||
|
def get_fresh_messages(self):
|
||||||
|
""" yield all fresh messages from all chats. """
|
||||||
|
dc_array = ffi.gc(
|
||||||
|
lib.dc_get_fresh_msgs(self._dc_context),
|
||||||
|
lib.dc_array_unref
|
||||||
|
)
|
||||||
|
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
|
||||||
|
|
||||||
def create_chat_by_contact(self, contact):
|
def create_chat_by_contact(self, contact):
|
||||||
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||||
@@ -272,6 +301,9 @@ class Account(object):
|
|||||||
""" create or get an existing chat object for the
|
""" create or get an existing chat object for the
|
||||||
the specified message.
|
the specified message.
|
||||||
|
|
||||||
|
If this message is in the deaddrop chat then
|
||||||
|
the sender will become an accepted contact.
|
||||||
|
|
||||||
:param message: messsage id or message instance.
|
:param message: messsage id or message instance.
|
||||||
:returns: a :class:`deltachat.chat.Chat` object.
|
:returns: a :class:`deltachat.chat.Chat` object.
|
||||||
"""
|
"""
|
||||||
@@ -324,6 +356,13 @@ class Account(object):
|
|||||||
"""
|
"""
|
||||||
return Message.from_db(self, msg_id)
|
return Message.from_db(self, msg_id)
|
||||||
|
|
||||||
|
def get_contact_by_id(self, contact_id):
|
||||||
|
""" return Contact instance or None.
|
||||||
|
:param contact_id: integer id of this contact.
|
||||||
|
:returns: None or :class:`deltachat.contact.Contact` instance.
|
||||||
|
"""
|
||||||
|
return Contact(self, contact_id)
|
||||||
|
|
||||||
def get_chat_by_id(self, chat_id):
|
def get_chat_by_id(self, chat_id):
|
||||||
""" return Chat instance.
|
""" return Chat instance.
|
||||||
:param chat_id: integer id of this chat.
|
:param chat_id: integer id of this chat.
|
||||||
@@ -368,13 +407,18 @@ class Account(object):
|
|||||||
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
|
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
|
||||||
|
|
||||||
def export_self_keys(self, path):
|
def export_self_keys(self, path):
|
||||||
""" export public and private keys to the specified directory. """
|
""" export public and private keys to the specified directory.
|
||||||
|
|
||||||
|
Note that the account does not have to be started.
|
||||||
|
"""
|
||||||
return self._export(path, imex_cmd=1)
|
return self._export(path, imex_cmd=1)
|
||||||
|
|
||||||
def export_all(self, path):
|
def export_all(self, path):
|
||||||
"""return new file containing a backup of all database state
|
"""return new file containing a backup of all database state
|
||||||
(chats, contacts, keys, media, ...). The file is created in the
|
(chats, contacts, keys, media, ...). The file is created in the
|
||||||
the `path` directory.
|
the `path` directory.
|
||||||
|
|
||||||
|
Note that the account does not have to be started.
|
||||||
"""
|
"""
|
||||||
export_files = self._export(path, 11)
|
export_files = self._export(path, 11)
|
||||||
if len(export_files) != 1:
|
if len(export_files) != 1:
|
||||||
@@ -382,7 +426,7 @@ class Account(object):
|
|||||||
return export_files[0]
|
return export_files[0]
|
||||||
|
|
||||||
def _export(self, path, imex_cmd):
|
def _export(self, path, imex_cmd):
|
||||||
with ImexTracker(self) as imex_tracker:
|
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||||
return imex_tracker.wait_finish()
|
return imex_tracker.wait_finish()
|
||||||
|
|
||||||
@@ -390,6 +434,8 @@ class Account(object):
|
|||||||
""" Import private keys found in the `path` directory.
|
""" Import private keys found in the `path` directory.
|
||||||
The last imported key is made the default keys unless its name
|
The last imported key is made the default keys unless its name
|
||||||
contains the string legacy. Public keys are not imported.
|
contains the string legacy. Public keys are not imported.
|
||||||
|
|
||||||
|
Note that the account does not have to be started.
|
||||||
"""
|
"""
|
||||||
self._import(path, imex_cmd=2)
|
self._import(path, imex_cmd=2)
|
||||||
|
|
||||||
@@ -402,7 +448,7 @@ class Account(object):
|
|||||||
self._import(path, imex_cmd=12)
|
self._import(path, imex_cmd=12)
|
||||||
|
|
||||||
def _import(self, path, imex_cmd):
|
def _import(self, path, imex_cmd):
|
||||||
with ImexTracker(self) as imex_tracker:
|
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||||
imex_tracker.wait_finish()
|
imex_tracker.wait_finish()
|
||||||
|
|
||||||
@@ -469,46 +515,6 @@ class Account(object):
|
|||||||
raise ValueError("could not join group")
|
raise ValueError("could not join group")
|
||||||
return Chat(self, chat_id)
|
return Chat(self, chat_id)
|
||||||
|
|
||||||
def stop_ongoing(self):
|
|
||||||
lib.dc_stop_ongoing_process(self._dc_context)
|
|
||||||
|
|
||||||
#
|
|
||||||
# meta API for start/stop and event based processing
|
|
||||||
#
|
|
||||||
|
|
||||||
def wait_next_incoming_message(self):
|
|
||||||
""" wait for and return next incoming message. """
|
|
||||||
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
|
||||||
return self.get_message_by_id(ev[2])
|
|
||||||
|
|
||||||
def start_threads(self):
|
|
||||||
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
|
|
||||||
|
|
||||||
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
|
|
||||||
:returns: None
|
|
||||||
"""
|
|
||||||
if not self.is_configured():
|
|
||||||
self.configure()
|
|
||||||
self._threads.start()
|
|
||||||
|
|
||||||
def stop_threads(self, wait=True):
|
|
||||||
""" stop IMAP/SMTP threads. """
|
|
||||||
if self._threads.is_started():
|
|
||||||
self.stop_ongoing()
|
|
||||||
self._threads.stop(wait=wait)
|
|
||||||
|
|
||||||
def shutdown(self, wait=True):
|
|
||||||
""" stop threads and close and remove underlying dc_context and callbacks. """
|
|
||||||
if hasattr(self, "_dc_context") and hasattr(self, "_threads"):
|
|
||||||
# print("SHUTDOWN", self)
|
|
||||||
self.stop_threads(wait=False)
|
|
||||||
lib.dc_close(self._dc_context)
|
|
||||||
self.stop_threads(wait=wait) # to wait for threads
|
|
||||||
deltachat.clear_context_callback(self._dc_context)
|
|
||||||
del self._dc_context
|
|
||||||
atexit.unregister(self.shutdown)
|
|
||||||
self.pluggy.unregister(self._evlogger)
|
|
||||||
|
|
||||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||||
"""set a new location. It effects all chats where we currently
|
"""set a new location. It effects all chats where we currently
|
||||||
have enabled location streaming.
|
have enabled location streaming.
|
||||||
@@ -523,88 +529,122 @@ class Account(object):
|
|||||||
if dc_res == 0:
|
if dc_res == 0:
|
||||||
raise ValueError("no chat is streaming locations")
|
raise ValueError("no chat is streaming locations")
|
||||||
|
|
||||||
|
#
|
||||||
|
# meta API for start/stop and event based processing
|
||||||
|
#
|
||||||
|
|
||||||
class ImexTracker:
|
def add_account_plugin(self, plugin, name=None):
|
||||||
def __init__(self, account):
|
""" add an account plugin which implements one or more of
|
||||||
self._imex_events = Queue()
|
the :class:`deltachat.hookspec.PerAccount` hooks.
|
||||||
self.account = account
|
"""
|
||||||
|
self._pm.register(plugin, name=name)
|
||||||
|
self._pm.check_pending()
|
||||||
|
return plugin
|
||||||
|
|
||||||
def __enter__(self):
|
@contextmanager
|
||||||
self.account.pluggy.register(self)
|
def temp_plugin(self, plugin):
|
||||||
return self
|
""" run a with-block with the given plugin temporarily registered. """
|
||||||
|
self._pm.register(plugin)
|
||||||
|
yield plugin
|
||||||
|
self._pm.unregister(plugin)
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def stop_ongoing(self):
|
||||||
self.account.pluggy.unregister(self)
|
""" Stop ongoing securejoin, configuration or other core jobs. """
|
||||||
|
lib.dc_stop_ongoing_process(self._dc_context)
|
||||||
|
|
||||||
@hookimpl
|
def start(self, callback_thread=True):
|
||||||
def process_low_level_event(self, account, event_name, data1, data2):
|
""" start this account (activate imap/smtp threads etc.)
|
||||||
# there could be multiple accounts instantiated
|
and return immediately.
|
||||||
if self.account is not account:
|
|
||||||
|
If this account is not configured, an internal configuration
|
||||||
|
job will be scheduled if config values are sufficiently specified.
|
||||||
|
|
||||||
|
You may call `wait_shutdown` or `shutdown` after the
|
||||||
|
account is in started mode.
|
||||||
|
|
||||||
|
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
|
||||||
|
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
if not self.is_configured():
|
||||||
|
if not self.get_config("addr") or not self.get_config("mail_pw"):
|
||||||
|
raise MissingCredentials("addr or mail_pwd not set in config")
|
||||||
|
lib.dc_configure(self._dc_context)
|
||||||
|
self._threads.start(callback_thread=callback_thread)
|
||||||
|
|
||||||
|
def wait_shutdown(self):
|
||||||
|
""" wait until shutdown of this account has completed. """
|
||||||
|
self._shutdown_event.wait()
|
||||||
|
|
||||||
|
def shutdown(self, wait=True):
|
||||||
|
""" shutdown account, stop threads and close and remove
|
||||||
|
underlying dc_context and callbacks. """
|
||||||
|
dc_context = self._dc_context
|
||||||
|
if dc_context is None:
|
||||||
return
|
return
|
||||||
if event_name == "DC_EVENT_IMEX_PROGRESS":
|
|
||||||
self._imex_events.put(data1)
|
|
||||||
elif event_name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
|
||||||
self._imex_events.put(data1)
|
|
||||||
|
|
||||||
def wait_finish(self, progress_timeout=60):
|
if self._threads.is_started():
|
||||||
""" Return list of written files, raise ValueError if ExportFailed. """
|
self.stop_ongoing()
|
||||||
files_written = []
|
self._threads.stop(wait=False)
|
||||||
while True:
|
lib.dc_close(dc_context)
|
||||||
ev = self._imex_events.get(timeout=progress_timeout)
|
self._hook_event_queue.put(None)
|
||||||
if isinstance(ev, str):
|
self._threads.stop(wait=wait) # to wait for threads
|
||||||
files_written.append(ev)
|
self._dc_context = None
|
||||||
elif ev == 0:
|
atexit.unregister(self.shutdown)
|
||||||
raise ValueError("export failed, exp-files: {}".format(files_written))
|
self._shutdown_event.set()
|
||||||
elif ev == 1000:
|
hook = hookspec.Global._get_plugin_manager().hook
|
||||||
return files_written
|
hook.dc_account_after_shutdown(account=self, dc_context=dc_context)
|
||||||
|
|
||||||
|
def _handle_current_events(self):
|
||||||
|
""" handle all currently queued events and then return. """
|
||||||
|
while 1:
|
||||||
|
try:
|
||||||
|
event = self._hook_event_queue.get(block=False)
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
event.call_hook()
|
||||||
|
|
||||||
class IOThreads:
|
def iter_events(self, timeout=None):
|
||||||
def __init__(self, dc_context, log_event=lambda *args: None):
|
""" yield hook events until shutdown.
|
||||||
self._dc_context = dc_context
|
|
||||||
self._thread_quitflag = False
|
|
||||||
self._name2thread = {}
|
|
||||||
self._log_event = log_event
|
|
||||||
self._log_running = True
|
|
||||||
|
|
||||||
# Make sure the current
|
It is not allowed to call iter_events() from multiple threads.
|
||||||
self._start_one_thread("deltachat-log", self.dc_thread_run)
|
"""
|
||||||
|
if self._in_use_iter_events:
|
||||||
|
raise RuntimeError("can only call iter_events() from one thread")
|
||||||
|
self._in_use_iter_events = True
|
||||||
|
while 1:
|
||||||
|
event = self._hook_event_queue.get(timeout=timeout)
|
||||||
|
if event is None:
|
||||||
|
break
|
||||||
|
yield event
|
||||||
|
|
||||||
def is_started(self):
|
def _map_ffi_event(self, ffi_event):
|
||||||
return lib.dc_is_open(self._dc_context) and lib.dc_is_running(self._dc_context)
|
name = ffi_event.name
|
||||||
|
if name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||||
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
|
data1 = ffi_event.data1
|
||||||
assert not self.is_started()
|
if data1 == 0 or data1 == 1000:
|
||||||
|
success = data1 == 1000
|
||||||
lib.dc_context_run(self._dc_context)
|
yield "ac_configure_completed", dict(success=success)
|
||||||
|
elif name == "DC_EVENT_INCOMING_MSG":
|
||||||
def _start_one_thread(self, name, func):
|
msg = self.get_message_by_id(ffi_event.data2)
|
||||||
self._name2thread[name] = t = threading.Thread(target=func, name=name)
|
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
||||||
t.setDaemon(1)
|
elif name == "DC_EVENT_MSGS_CHANGED":
|
||||||
t.start()
|
if ffi_event.data2 != 0:
|
||||||
|
msg = self.get_message_by_id(ffi_event.data2)
|
||||||
def stop(self, wait=False):
|
if msg.is_outgoing():
|
||||||
if self.is_started():
|
res = map_system_message(msg)
|
||||||
lib.dc_context_shutdown(self._dc_context)
|
if res and res[0].startswith("ac_member"):
|
||||||
|
yield res
|
||||||
def dc_thread_run(self):
|
yield "ac_outgoing_message", dict(message=msg)
|
||||||
self._log_event("py-bindings-info", 0, "DC LOG THREAD START")
|
elif msg.is_in_fresh():
|
||||||
|
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
||||||
while self._log_running:
|
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||||
if lib.dc_is_open(self._dc_context) and lib.dc_has_next_event(self._dc_context):
|
msg = self.get_message_by_id(ffi_event.data2)
|
||||||
event = lib.dc_get_next_event(self._dc_context)
|
yield "ac_message_delivered", dict(message=msg)
|
||||||
if event != ffi.NULL:
|
elif name == "DC_EVENT_CHAT_MODIFIED":
|
||||||
deltachat.py_dc_callback(
|
chat = self.get_chat_by_id(ffi_event.data1)
|
||||||
self._dc_context,
|
yield "ac_chat_modified", dict(chat=chat)
|
||||||
lib.dc_event_get_id(event),
|
|
||||||
lib.dc_event_get_data1(event),
|
|
||||||
lib.dc_event_get_data2(event)
|
|
||||||
)
|
|
||||||
lib.dc_event_unref(event)
|
|
||||||
else:
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
self._log_event("py-bindings-info", 0, "DC LOG THREAD FINISHED")
|
|
||||||
|
|
||||||
|
|
||||||
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
||||||
@@ -631,3 +671,17 @@ class ScannedQRCode:
|
|||||||
@property
|
@property
|
||||||
def contact_id(self):
|
def contact_id(self):
|
||||||
return self._dc_lot.id()
|
return self._dc_lot.id()
|
||||||
|
|
||||||
|
|
||||||
|
class HookEvent:
|
||||||
|
def __init__(self, account, name, kwargs):
|
||||||
|
assert hasattr(account._pm.hook, name), name
|
||||||
|
self.account = account
|
||||||
|
self.name = name
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
def call_hook(self):
|
||||||
|
hook = getattr(self.account._pm.hook, self.name, None)
|
||||||
|
if hook is None:
|
||||||
|
raise ValueError("event_name {} unknown".format(self.name))
|
||||||
|
return hook(**self.kwargs)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Chat(object):
|
|||||||
return not (self == other)
|
return not (self == other)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Chat id={} name={} dc_context={}>".format(self.id, self.get_name(), self._dc_context)
|
return "<Chat id={} name={}>".format(self.id, self.get_name())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _dc_chat(self):
|
def _dc_chat(self):
|
||||||
@@ -51,6 +51,16 @@ class Chat(object):
|
|||||||
|
|
||||||
# ------ chat status/metadata API ------------------------------
|
# ------ chat status/metadata API ------------------------------
|
||||||
|
|
||||||
|
def is_group(self):
|
||||||
|
""" 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.
|
||||||
|
"""
|
||||||
|
return lib.dc_chat_get_type(self._dc_chat) in (
|
||||||
|
const.DC_CHAT_TYPE_GROUP,
|
||||||
|
const.DC_CHAT_TYPE_VERIFIED_GROUP
|
||||||
|
)
|
||||||
|
|
||||||
def is_deaddrop(self):
|
def is_deaddrop(self):
|
||||||
""" return true if this chat is a deaddrop chat.
|
""" return true if this chat is a deaddrop chat.
|
||||||
|
|
||||||
@@ -129,7 +139,7 @@ class Chat(object):
|
|||||||
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
|
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
|
||||||
|
|
||||||
def get_type(self):
|
def get_type(self):
|
||||||
""" return type of this chat.
|
""" (deprecated) return type of this chat.
|
||||||
|
|
||||||
:returns: one of const.DC_CHAT_TYPE_*
|
:returns: one of const.DC_CHAT_TYPE_*
|
||||||
"""
|
"""
|
||||||
@@ -353,7 +363,7 @@ class Chat(object):
|
|||||||
lib.dc_array_unref
|
lib.dc_array_unref
|
||||||
)
|
)
|
||||||
return list(iter_array(
|
return list(iter_array(
|
||||||
dc_array, lambda id: Contact(self._dc_context, id))
|
dc_array, lambda id: Contact(self.account, id))
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_profile_image(self, img_path):
|
def set_profile_image(self, img_path):
|
||||||
@@ -405,12 +415,6 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
return lib.dc_chat_get_color(self._dc_chat)
|
return lib.dc_chat_get_color(self._dc_chat)
|
||||||
|
|
||||||
def get_subtitle(self):
|
|
||||||
"""return the subtitle of the chat
|
|
||||||
:returns: the subtitle
|
|
||||||
"""
|
|
||||||
return from_dc_charpointer(lib.dc_chat_get_subtitle(self._dc_chat))
|
|
||||||
|
|
||||||
# ------ location streaming API ------------------------------
|
# ------ location streaming API ------------------------------
|
||||||
|
|
||||||
def is_sending_locations(self):
|
def is_sending_locations(self):
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from os.path import join as joinpath
|
|||||||
DC_GCL_ARCHIVED_ONLY = 0x01
|
DC_GCL_ARCHIVED_ONLY = 0x01
|
||||||
DC_GCL_NO_SPECIALS = 0x02
|
DC_GCL_NO_SPECIALS = 0x02
|
||||||
DC_GCL_ADD_ALLDONE_HINT = 0x04
|
DC_GCL_ADD_ALLDONE_HINT = 0x04
|
||||||
|
DC_GCL_FOR_FORWARDING = 0x08
|
||||||
DC_GCL_VERIFIED_ONLY = 0x01
|
DC_GCL_VERIFIED_ONLY = 0x01
|
||||||
DC_GCL_ADD_SELF = 0x02
|
DC_GCL_ADD_SELF = 0x02
|
||||||
DC_QR_ASK_VERIFYCONTACT = 200
|
DC_QR_ASK_VERIFYCONTACT = 200
|
||||||
@@ -98,7 +99,6 @@ DC_EVENT_IMEX_PROGRESS = 2051
|
|||||||
DC_EVENT_IMEX_FILE_WRITTEN = 2052
|
DC_EVENT_IMEX_FILE_WRITTEN = 2052
|
||||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
|
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
|
||||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
|
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
|
||||||
DC_EVENT_SECUREJOIN_MEMBER_ADDED = 2062
|
|
||||||
DC_EVENT_FILE_COPIED = 2055
|
DC_EVENT_FILE_COPIED = 2055
|
||||||
DC_EVENT_IS_OFFLINE = 2081
|
DC_EVENT_IS_OFFLINE = 2081
|
||||||
DC_EVENT_GET_STRING = 2091
|
DC_EVENT_GET_STRING = 2091
|
||||||
@@ -115,8 +115,6 @@ DC_CHAT_VISIBILITY_PINNED = 2
|
|||||||
DC_STR_NOMESSAGES = 1
|
DC_STR_NOMESSAGES = 1
|
||||||
DC_STR_SELF = 2
|
DC_STR_SELF = 2
|
||||||
DC_STR_DRAFT = 3
|
DC_STR_DRAFT = 3
|
||||||
DC_STR_MEMBER = 4
|
|
||||||
DC_STR_CONTACT = 6
|
|
||||||
DC_STR_VOICEMESSAGE = 7
|
DC_STR_VOICEMESSAGE = 7
|
||||||
DC_STR_DEADDROP = 8
|
DC_STR_DEADDROP = 8
|
||||||
DC_STR_IMAGE = 9
|
DC_STR_IMAGE = 9
|
||||||
@@ -148,7 +146,6 @@ DC_STR_ARCHIVEDCHATS = 40
|
|||||||
DC_STR_STARREDMSGS = 41
|
DC_STR_STARREDMSGS = 41
|
||||||
DC_STR_AC_SETUP_MSG_SUBJECT = 42
|
DC_STR_AC_SETUP_MSG_SUBJECT = 42
|
||||||
DC_STR_AC_SETUP_MSG_BODY = 43
|
DC_STR_AC_SETUP_MSG_BODY = 43
|
||||||
DC_STR_SELFTALK_SUBTITLE = 50
|
|
||||||
DC_STR_CANNOT_LOGIN = 60
|
DC_STR_CANNOT_LOGIN = 60
|
||||||
DC_STR_SERVER_RESPONSE = 61
|
DC_STR_SERVER_RESPONSE = 61
|
||||||
DC_STR_MSGACTIONBYUSER = 62
|
DC_STR_MSGACTIONBYUSER = 62
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ class Contact(object):
|
|||||||
|
|
||||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||||
"""
|
"""
|
||||||
def __init__(self, dc_context, id):
|
def __init__(self, account, id):
|
||||||
self._dc_context = dc_context
|
self.account = account
|
||||||
|
self._dc_context = account._dc_context
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
@@ -57,3 +58,7 @@ class Contact(object):
|
|||||||
if dc_res == ffi.NULL:
|
if dc_res == ffi.NULL:
|
||||||
return None
|
return None
|
||||||
return from_dc_charpointer(dc_res)
|
return from_dc_charpointer(dc_res)
|
||||||
|
|
||||||
|
def get_chat(self):
|
||||||
|
"""return 1:1 chat for this contact. """
|
||||||
|
return self.account.create_chat_by_contact(self)
|
||||||
|
|||||||
@@ -1,28 +1,88 @@
|
|||||||
|
import deltachat
|
||||||
import threading
|
import threading
|
||||||
import re
|
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
from queue import Queue, Empty
|
from queue import Queue, Empty
|
||||||
from .hookspec import hookimpl
|
from .hookspec import account_hookimpl, global_hookimpl
|
||||||
|
|
||||||
|
|
||||||
class EventLogger:
|
@global_hookimpl
|
||||||
|
def dc_account_init(account):
|
||||||
|
# send all FFI events for this account to a plugin hook
|
||||||
|
def _ll_event(ctx, evt_name, data1, data2):
|
||||||
|
assert ctx == account._dc_context
|
||||||
|
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
|
||||||
|
account._pm.hook.ac_process_ffi_event(
|
||||||
|
account=account, ffi_event=ffi_event
|
||||||
|
)
|
||||||
|
deltachat.set_context_callback(account._dc_context, _ll_event)
|
||||||
|
|
||||||
|
|
||||||
|
@global_hookimpl
|
||||||
|
def dc_account_after_shutdown(dc_context):
|
||||||
|
deltachat.clear_context_callback(dc_context)
|
||||||
|
|
||||||
|
|
||||||
|
class FFIEvent:
|
||||||
|
def __init__(self, name, data1, data2):
|
||||||
|
self.name = name
|
||||||
|
self.data1 = data1
|
||||||
|
self.data2 = data2
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
class FFIEventLogger:
|
||||||
|
""" If you register an instance of this logger with an Account
|
||||||
|
you'll get all ffi-events printed.
|
||||||
|
"""
|
||||||
|
# to prevent garbled logging
|
||||||
_loglock = threading.RLock()
|
_loglock = threading.RLock()
|
||||||
|
|
||||||
def __init__(self, account, logid=None, debug=True):
|
def __init__(self, account, logid):
|
||||||
|
"""
|
||||||
|
:param logid: an optional logging prefix that should be used with
|
||||||
|
the default internal logging.
|
||||||
|
"""
|
||||||
self.account = account
|
self.account = account
|
||||||
self._event_queue = Queue()
|
|
||||||
self._debug = debug
|
|
||||||
if logid is None:
|
|
||||||
logid = str(self.account._dc_context).strip(">").split()[-1]
|
|
||||||
self.logid = logid
|
self.logid = logid
|
||||||
self._timeout = None
|
|
||||||
self.init_time = time.time()
|
self.init_time = time.time()
|
||||||
|
|
||||||
@hookimpl
|
@account_hookimpl
|
||||||
def process_low_level_event(self, account, event_name, data1, data2):
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
if self.account == account:
|
self._log_event(ffi_event)
|
||||||
self._log_event(event_name, data1, data2)
|
|
||||||
self._event_queue.put((event_name, data1, data2))
|
def _log_event(self, ffi_event):
|
||||||
|
# don't show events that are anyway empty impls now
|
||||||
|
if ffi_event.name == "DC_EVENT_GET_STRING":
|
||||||
|
return
|
||||||
|
self.account.ac_log_line(str(ffi_event))
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_log_line(self, message):
|
||||||
|
t = threading.currentThread()
|
||||||
|
tname = getattr(t, "name", t)
|
||||||
|
if tname == "MainThread":
|
||||||
|
tname = "MAIN"
|
||||||
|
elapsed = time.time() - self.init_time
|
||||||
|
locname = tname
|
||||||
|
if self.logid:
|
||||||
|
locname += "-" + self.logid
|
||||||
|
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
|
||||||
|
with self._loglock:
|
||||||
|
print(s, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FFIEventTracker:
|
||||||
|
def __init__(self, account, timeout=None):
|
||||||
|
self.account = account
|
||||||
|
self._timeout = timeout
|
||||||
|
self._event_queue = Queue()
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
|
self._event_queue.put(ffi_event)
|
||||||
|
|
||||||
def set_timeout(self, timeout):
|
def set_timeout(self, timeout):
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
@@ -32,10 +92,10 @@ class EventLogger:
|
|||||||
self.get(check_error=check_error)
|
self.get(check_error=check_error)
|
||||||
|
|
||||||
def get(self, timeout=None, check_error=True):
|
def get(self, timeout=None, check_error=True):
|
||||||
timeout = timeout or self._timeout
|
timeout = timeout if timeout is not None else self._timeout
|
||||||
ev = self._event_queue.get(timeout=timeout)
|
ev = self._event_queue.get(timeout=timeout)
|
||||||
if check_error and ev[0] == "DC_EVENT_ERROR":
|
if check_error and ev.name == "DC_EVENT_ERROR":
|
||||||
raise ValueError("{}({!r},{!r})".format(*ev))
|
raise ValueError(str(ev))
|
||||||
return ev
|
return ev
|
||||||
|
|
||||||
def ensure_event_not_queued(self, event_name_regex):
|
def ensure_event_not_queued(self, event_name_regex):
|
||||||
@@ -47,35 +107,31 @@ class EventLogger:
|
|||||||
except Empty:
|
except Empty:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
assert not rex.match(ev[0]), "event found {}".format(ev)
|
assert not rex.match(ev.name), "event found {}".format(ev)
|
||||||
|
|
||||||
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
||||||
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
self.account.ac_log_line("-- waiting for event with regex: {} --".format(event_name_regex))
|
||||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||||
while 1:
|
while 1:
|
||||||
ev = self.get(timeout=timeout, check_error=check_error)
|
ev = self.get(timeout=timeout, check_error=check_error)
|
||||||
if rex.match(ev[0]):
|
if rex.match(ev.name):
|
||||||
return ev
|
return ev
|
||||||
|
|
||||||
def get_info_matching(self, regex):
|
def get_info_matching(self, regex):
|
||||||
rex = re.compile("(?:{}).*".format(regex))
|
rex = re.compile("(?:{}).*".format(regex))
|
||||||
while 1:
|
while 1:
|
||||||
ev = self.get_matching("DC_EVENT_INFO")
|
ev = self.get_matching("DC_EVENT_INFO")
|
||||||
if rex.match(ev[2]):
|
if rex.match(ev.data2):
|
||||||
return ev
|
return ev
|
||||||
|
|
||||||
def _log_event(self, evt_name, data1, data2):
|
def wait_next_incoming_message(self):
|
||||||
# don't show events that are anyway empty impls now
|
""" wait for and return next incoming message. """
|
||||||
if evt_name == "DC_EVENT_GET_STRING":
|
ev = self.get_matching("DC_EVENT_INCOMING_MSG")
|
||||||
return
|
return self.account.get_message_by_id(ev.data2)
|
||||||
if self._debug:
|
|
||||||
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
|
|
||||||
self._log(evpart)
|
|
||||||
|
|
||||||
def _log(self, msg):
|
def wait_next_messages_changed(self):
|
||||||
t = threading.currentThread()
|
""" wait for and return next message-changed message or None
|
||||||
tname = getattr(t, "name", t)
|
if the event contains no msgid"""
|
||||||
if tname == "MainThread":
|
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
tname = "MAIN"
|
if ev.data2 > 0:
|
||||||
with self._loglock:
|
return self.account.get_message_by_id(ev.data2)
|
||||||
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))
|
|
||||||
|
|||||||
@@ -1,25 +1,92 @@
|
|||||||
""" Hooks for python bindings """
|
""" Hooks for Python bindings to Delta Chat Core Rust CFFI"""
|
||||||
|
|
||||||
import pluggy
|
import pluggy
|
||||||
|
|
||||||
name = "deltachat"
|
|
||||||
|
|
||||||
hookspec = pluggy.HookspecMarker(name)
|
account_spec_name = "deltachat-account"
|
||||||
hookimpl = pluggy.HookimplMarker(name)
|
account_hookspec = pluggy.HookspecMarker(account_spec_name)
|
||||||
_plugin_manager = None
|
account_hookimpl = pluggy.HookimplMarker(account_spec_name)
|
||||||
|
|
||||||
|
global_spec_name = "deltachat-global"
|
||||||
|
global_hookspec = pluggy.HookspecMarker(global_spec_name)
|
||||||
|
global_hookimpl = pluggy.HookimplMarker(global_spec_name)
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_manager():
|
class PerAccount:
|
||||||
global _plugin_manager
|
""" per-Account-instance hook specifications.
|
||||||
if _plugin_manager is None:
|
|
||||||
_plugin_manager = pluggy.PluginManager(name)
|
Except for ac_process_ffi_event all hooks are executed
|
||||||
_plugin_manager.add_hookspecs(DeltaChatHookSpecs)
|
in the thread which calls Account.wait_shutdown().
|
||||||
return _plugin_manager
|
"""
|
||||||
|
@classmethod
|
||||||
|
def _make_plugin_manager(cls):
|
||||||
|
pm = pluggy.PluginManager(account_spec_name)
|
||||||
|
pm.add_hookspecs(cls)
|
||||||
|
return pm
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
|
""" process a CFFI low level events for a given account.
|
||||||
|
|
||||||
|
ffi_event has "name", "data1", "data2" values as specified
|
||||||
|
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
|
||||||
|
|
||||||
|
DANGER: this hook is executed from the callback invoked by core.
|
||||||
|
Hook implementations need to be short running and can typically
|
||||||
|
not call back into core because this would easily cause recursion issues.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_log_line(self, message):
|
||||||
|
""" log a message related to the account. """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_configure_completed(self, success):
|
||||||
|
""" Called when a configure process completed. """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_incoming_message(self, message):
|
||||||
|
""" Called on any incoming message (to deaddrop or chat). """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_outgoing_message(self, message):
|
||||||
|
""" Called on each outgoing message (both system and "normal")."""
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_message_delivered(self, message):
|
||||||
|
""" Called when an outgoing message has been delivered to SMTP. """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_chat_modified(self, chat):
|
||||||
|
""" Chat was created or modified regarding membership, avatar, title. """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_member_added(self, chat, contact, message):
|
||||||
|
""" Called for each contact added to an accepted chat. """
|
||||||
|
|
||||||
|
@account_hookspec
|
||||||
|
def ac_member_removed(self, chat, contact, message):
|
||||||
|
""" Called for each contact removed from a chat. """
|
||||||
|
|
||||||
|
|
||||||
class DeltaChatHookSpecs:
|
class Global:
|
||||||
""" Plugin Hook specifications for Python bindings to Delta Chat CFFI. """
|
""" global hook specifications using a per-process singleton
|
||||||
|
plugin manager instance.
|
||||||
|
|
||||||
@hookspec
|
"""
|
||||||
def process_low_level_event(self, account, event_name, data1, data2):
|
_plugin_manager = None
|
||||||
""" process a CFFI low level events for a given account. """
|
|
||||||
|
@classmethod
|
||||||
|
def _get_plugin_manager(cls):
|
||||||
|
if cls._plugin_manager is None:
|
||||||
|
cls._plugin_manager = pm = pluggy.PluginManager(global_spec_name)
|
||||||
|
pm.add_hookspecs(cls)
|
||||||
|
return cls._plugin_manager
|
||||||
|
|
||||||
|
@global_hookspec
|
||||||
|
def dc_account_init(self, account):
|
||||||
|
""" called when `Account::__init__()` function starts executing. """
|
||||||
|
|
||||||
|
@global_hookspec
|
||||||
|
def dc_account_after_shutdown(self, account, dc_context):
|
||||||
|
""" Called after the account has been shutdown. """
|
||||||
|
|||||||
106
python/src/deltachat/iothreads.py
Normal file
106
python/src/deltachat/iothreads.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from .capi import lib
|
||||||
|
|
||||||
|
|
||||||
|
class IOThreads:
|
||||||
|
def __init__(self, account):
|
||||||
|
self.account = account
|
||||||
|
self._dc_context = account._dc_context
|
||||||
|
self._thread_quitflag = False
|
||||||
|
self._name2thread = {}
|
||||||
|
|
||||||
|
def is_started(self):
|
||||||
|
return len(self._name2thread) > 0
|
||||||
|
|
||||||
|
def start(self, callback_thread):
|
||||||
|
assert not self.is_started()
|
||||||
|
self._start_one_thread("inbox", self.imap_thread_run)
|
||||||
|
self._start_one_thread("smtp", self.smtp_thread_run)
|
||||||
|
|
||||||
|
if callback_thread:
|
||||||
|
self._start_one_thread("cb", self.cb_thread_run)
|
||||||
|
|
||||||
|
if int(self.account.get_config("mvbox_watch")):
|
||||||
|
self._start_one_thread("mvbox", self.mvbox_thread_run)
|
||||||
|
|
||||||
|
if int(self.account.get_config("sentbox_watch")):
|
||||||
|
self._start_one_thread("sentbox", self.sentbox_thread_run)
|
||||||
|
|
||||||
|
def _start_one_thread(self, name, func):
|
||||||
|
self._name2thread[name] = t = threading.Thread(target=func, name=name)
|
||||||
|
t.setDaemon(1)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def log_execution(self, message):
|
||||||
|
self.account.ac_log_line(message + " START")
|
||||||
|
yield
|
||||||
|
self.account.ac_log_line(message + " FINISHED")
|
||||||
|
|
||||||
|
def stop(self, wait=False):
|
||||||
|
self._thread_quitflag = True
|
||||||
|
|
||||||
|
# Workaround for a race condition. Make sure that thread is
|
||||||
|
# not in between checking for quitflag and entering idle.
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
lib.dc_interrupt_imap_idle(self._dc_context)
|
||||||
|
lib.dc_interrupt_smtp_idle(self._dc_context)
|
||||||
|
if "mvbox" in self._name2thread:
|
||||||
|
lib.dc_interrupt_mvbox_idle(self._dc_context)
|
||||||
|
if "sentbox" in self._name2thread:
|
||||||
|
lib.dc_interrupt_sentbox_idle(self._dc_context)
|
||||||
|
if wait:
|
||||||
|
for name, thread in self._name2thread.items():
|
||||||
|
if thread != threading.currentThread():
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
def cb_thread_run(self):
|
||||||
|
with self.log_execution("CALLBACK THREAD START"):
|
||||||
|
it = self.account.iter_events()
|
||||||
|
while not self._thread_quitflag:
|
||||||
|
try:
|
||||||
|
ev = next(it)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
self.account.ac_log_line("calling hook name={} kwargs={}".format(ev.name, ev.kwargs))
|
||||||
|
ev.call_hook()
|
||||||
|
|
||||||
|
def imap_thread_run(self):
|
||||||
|
with self.log_execution("INBOX THREAD START"):
|
||||||
|
while not self._thread_quitflag:
|
||||||
|
lib.dc_perform_imap_jobs(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_imap_fetch(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_imap_idle(self._dc_context)
|
||||||
|
|
||||||
|
def mvbox_thread_run(self):
|
||||||
|
with self.log_execution("MVBOX THREAD"):
|
||||||
|
while not self._thread_quitflag:
|
||||||
|
lib.dc_perform_mvbox_jobs(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_mvbox_fetch(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_mvbox_idle(self._dc_context)
|
||||||
|
|
||||||
|
def sentbox_thread_run(self):
|
||||||
|
with self.log_execution("SENTBOX THREAD"):
|
||||||
|
while not self._thread_quitflag:
|
||||||
|
lib.dc_perform_sentbox_jobs(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_sentbox_fetch(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_sentbox_idle(self._dc_context)
|
||||||
|
|
||||||
|
def smtp_thread_run(self):
|
||||||
|
with self.log_execution("SMTP THREAD"):
|
||||||
|
while not self._thread_quitflag:
|
||||||
|
lib.dc_perform_smtp_jobs(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_smtp_idle(self._dc_context)
|
||||||
@@ -28,7 +28,9 @@ class Message(object):
|
|||||||
return self.account == other.account and self.id == other.id
|
return self.account == other.account and self.id == other.id
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Message id={} dc_context={}>".format(self.id, self._dc_context)
|
c = self.get_sender_contact()
|
||||||
|
return "<Message id={} sender={}/{} outgoing={} chat={}/{}>".format(
|
||||||
|
self.id, c.id, c.addr, self.is_outgoing(), self.chat.id, self.chat.get_name())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_db(cls, account, id):
|
def from_db(cls, account, id):
|
||||||
@@ -50,6 +52,16 @@ class Message(object):
|
|||||||
lib.dc_msg_unref
|
lib.dc_msg_unref
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def accept_sender_contact(self):
|
||||||
|
""" ensure that the sender is an accepted contact
|
||||||
|
and that the message has a non-deaddrop chat object.
|
||||||
|
"""
|
||||||
|
self.account.create_chat_by_message(self)
|
||||||
|
self._dc_msg = ffi.gc(
|
||||||
|
lib.dc_get_msg(self._dc_context, self.id),
|
||||||
|
lib.dc_msg_unref
|
||||||
|
)
|
||||||
|
|
||||||
@props.with_doc
|
@props.with_doc
|
||||||
def text(self):
|
def text(self):
|
||||||
"""unicode text of this messages (might be empty if not a text message). """
|
"""unicode text of this messages (might be empty if not a text message). """
|
||||||
@@ -81,6 +93,10 @@ class Message(object):
|
|||||||
"""mime type of the file (if it exists)"""
|
"""mime type of the file (if it exists)"""
|
||||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||||
|
|
||||||
|
def is_system_message(self):
|
||||||
|
""" return True if this message is a system/info message. """
|
||||||
|
return lib.dc_msg_is_info(self._dc_msg)
|
||||||
|
|
||||||
def is_setup_message(self):
|
def is_setup_message(self):
|
||||||
""" return True if this message is a setup message. """
|
""" return True if this message is a setup message. """
|
||||||
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
return lib.dc_msg_is_setupmessage(self._dc_msg)
|
||||||
@@ -159,6 +175,13 @@ class Message(object):
|
|||||||
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
||||||
return Chat(self.account, chat_id)
|
return Chat(self.account, chat_id)
|
||||||
|
|
||||||
|
def get_sender_chat(self):
|
||||||
|
"""return the 1:1 chat with the sender of this message.
|
||||||
|
|
||||||
|
:returns: :class:`deltachat.chat.Chat` instance
|
||||||
|
"""
|
||||||
|
return self.get_sender_contact().get_chat()
|
||||||
|
|
||||||
def get_sender_contact(self):
|
def get_sender_contact(self):
|
||||||
"""return the contact of who wrote the message.
|
"""return the contact of who wrote the message.
|
||||||
|
|
||||||
@@ -166,7 +189,7 @@ class Message(object):
|
|||||||
"""
|
"""
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||||
return Contact(self._dc_context, contact_id)
|
return Contact(self.account, contact_id)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Message State query methods
|
# Message State query methods
|
||||||
@@ -207,6 +230,13 @@ class Message(object):
|
|||||||
"""
|
"""
|
||||||
return self._msgstate == const.DC_STATE_IN_SEEN
|
return self._msgstate == const.DC_STATE_IN_SEEN
|
||||||
|
|
||||||
|
def is_outgoing(self):
|
||||||
|
"""Return True if Message is outgoing. """
|
||||||
|
return self._msgstate in (
|
||||||
|
const.DC_STATE_OUT_PREPARING, const.DC_STATE_OUT_PENDING,
|
||||||
|
const.DC_STATE_OUT_FAILED, const.DC_STATE_OUT_MDN_RCVD,
|
||||||
|
const.DC_STATE_OUT_DELIVERED)
|
||||||
|
|
||||||
def is_out_preparing(self):
|
def is_out_preparing(self):
|
||||||
"""Return True if Message is outgoing, but its file is being prepared.
|
"""Return True if Message is outgoing, but its file is being prepared.
|
||||||
"""
|
"""
|
||||||
@@ -269,6 +299,10 @@ class Message(object):
|
|||||||
""" return True if it's a file message. """
|
""" return True if it's a file message. """
|
||||||
return self._view_type == const.DC_MSG_FILE
|
return self._view_type == const.DC_MSG_FILE
|
||||||
|
|
||||||
|
def mark_seen(self):
|
||||||
|
""" mark this message as seen. """
|
||||||
|
self.account.mark_seen_messages([self.id])
|
||||||
|
|
||||||
|
|
||||||
# some code for handling DC_MSG_* view types
|
# some code for handling DC_MSG_* view types
|
||||||
|
|
||||||
@@ -288,3 +322,29 @@ def get_viewtype_code_from_name(view_type_name):
|
|||||||
return code
|
return code
|
||||||
raise ValueError("message typecode not found for {!r}, "
|
raise ValueError("message typecode not found for {!r}, "
|
||||||
"available {!r}".format(view_type_name, list(_view_type_mapping.values())))
|
"available {!r}".format(view_type_name, list(_view_type_mapping.values())))
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# some helper code for turning system messages into hook events
|
||||||
|
#
|
||||||
|
|
||||||
|
def map_system_message(msg):
|
||||||
|
if msg.is_system_message():
|
||||||
|
res = parse_system_add_remove(msg.text)
|
||||||
|
if res:
|
||||||
|
contact = msg.account.get_contact_by_addr(res[1])
|
||||||
|
if contact:
|
||||||
|
d = dict(chat=msg.chat, contact=contact, message=msg)
|
||||||
|
return "ac_member_" + res[0], d
|
||||||
|
|
||||||
|
|
||||||
|
def parse_system_add_remove(text):
|
||||||
|
# Member Me (x@y) removed by a@b.
|
||||||
|
# Member x@y removed by a@b
|
||||||
|
text = text.lower()
|
||||||
|
parts = text.split()
|
||||||
|
if parts[0] == "member":
|
||||||
|
if parts[2] in ("removed", "added"):
|
||||||
|
return parts[2], parts[1]
|
||||||
|
if parts[3] in ("removed", "added"):
|
||||||
|
return parts[3], parts[2].strip("()")
|
||||||
|
|||||||
381
python/src/deltachat/testplugin.py
Normal file
381
python/src/deltachat/testplugin.py
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
import fnmatch
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
from . import Account, const
|
||||||
|
from .tracker import ConfigureTracker
|
||||||
|
from .capi import lib
|
||||||
|
from .eventlogger import FFIEventLogger, FFIEventTracker
|
||||||
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
|
from _pytest._code import Source
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.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(
|
||||||
|
"--ignored", action="store_true",
|
||||||
|
help="Also run tests marked with the ignored marker",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure(config):
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
|
||||||
|
)
|
||||||
|
cfg = config.getoption('--liveconfig')
|
||||||
|
if not cfg:
|
||||||
|
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
|
||||||
|
if cfg:
|
||||||
|
config.option.liveconfig = cfg
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_runtest_setup(item):
|
||||||
|
if (list(item.iter_markers(name="ignored"))
|
||||||
|
and not item.config.getoption("ignored")):
|
||||||
|
pytest.skip("Ignored tests not requested, use --ignored")
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_report_header(config, startdir):
|
||||||
|
summary = []
|
||||||
|
|
||||||
|
t = tempfile.mktemp()
|
||||||
|
m = MonkeyPatch()
|
||||||
|
try:
|
||||||
|
m.setattr(sys.stdout, "write", lambda x: len(x))
|
||||||
|
ac = Account(t)
|
||||||
|
info = ac.get_info()
|
||||||
|
ac.shutdown()
|
||||||
|
finally:
|
||||||
|
m.undo()
|
||||||
|
os.remove(t)
|
||||||
|
summary.extend(['Deltachat core={} sqlite={}'.format(
|
||||||
|
info['deltachat_core_version'],
|
||||||
|
info['sqlite_version'],
|
||||||
|
)])
|
||||||
|
|
||||||
|
cfg = config.option.liveconfig
|
||||||
|
if cfg:
|
||||||
|
if "?" in cfg:
|
||||||
|
url, token = cfg.split("?", 1)
|
||||||
|
summary.append('Liveconfig provider: {}?<token ommitted>'.format(url))
|
||||||
|
else:
|
||||||
|
summary.append('Liveconfig file: {}'.format(cfg))
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
class SessionLiveConfigFromFile:
|
||||||
|
def __init__(self, fn):
|
||||||
|
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):
|
||||||
|
return self.configlist[index]
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
return bool(self.configlist)
|
||||||
|
|
||||||
|
|
||||||
|
class SessionLiveConfigFromURL:
|
||||||
|
def __init__(self, url):
|
||||||
|
self.configlist = []
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
def get(self, index):
|
||||||
|
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 {!r}".format(res))
|
||||||
|
d = res.json()
|
||||||
|
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||||
|
self.configlist.append(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
return SessionLiveConfigFromFile(liveconfig_opt)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def data(request):
|
||||||
|
class Data:
|
||||||
|
def __init__(self):
|
||||||
|
# trying to find test data heuristically
|
||||||
|
# because we are run from a dev-setup with pytest direct,
|
||||||
|
# through tox, and then maybe also from deltachat-binding
|
||||||
|
# users like "deltabot".
|
||||||
|
self.paths = [os.path.normpath(x) for x in [
|
||||||
|
os.path.join(os.path.dirname(request.fspath.strpath), "data"),
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data")
|
||||||
|
]]
|
||||||
|
|
||||||
|
def get_path(self, bn):
|
||||||
|
""" return path of file or None if it doesn't exist. """
|
||||||
|
for path in self.paths:
|
||||||
|
fn = os.path.join(path, *bn.split("/"))
|
||||||
|
if os.path.exists(fn):
|
||||||
|
return fn
|
||||||
|
print("WARNING: path does not exist: {!r}".format(fn))
|
||||||
|
|
||||||
|
def read_path(self, bn, mode="r"):
|
||||||
|
fn = self.get_path(bn)
|
||||||
|
if fn is not None:
|
||||||
|
with open(fn, mode) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
return Data()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||||
|
|
||||||
|
class AccountMaker:
|
||||||
|
def __init__(self):
|
||||||
|
self.live_count = 0
|
||||||
|
self.offline_count = 0
|
||||||
|
self._finalizers = []
|
||||||
|
self.init_time = time.time()
|
||||||
|
self._generated_keys = ["alice", "bob", "charlie",
|
||||||
|
"dom", "elena", "fiona"]
|
||||||
|
|
||||||
|
def finalize(self):
|
||||||
|
while self._finalizers:
|
||||||
|
fin = self._finalizers.pop()
|
||||||
|
fin()
|
||||||
|
|
||||||
|
def make_account(self, path, logid, quiet=False):
|
||||||
|
ac = Account(path)
|
||||||
|
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||||
|
ac._configtracker = ac.add_account_plugin(ConfigureTracker())
|
||||||
|
if not quiet:
|
||||||
|
ac.add_account_plugin(FFIEventLogger(ac, logid=logid))
|
||||||
|
self._finalizers.append(ac.shutdown)
|
||||||
|
return ac
|
||||||
|
|
||||||
|
def get_unconfigured_account(self):
|
||||||
|
self.offline_count += 1
|
||||||
|
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||||
|
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||||
|
ac._evtracker.init_time = self.init_time
|
||||||
|
ac._evtracker.set_timeout(2)
|
||||||
|
return ac
|
||||||
|
|
||||||
|
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_configured_offline_account(self):
|
||||||
|
ac = self.get_unconfigured_account()
|
||||||
|
|
||||||
|
# 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 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"
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
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'])
|
||||||
|
ac._evtracker.init_time = self.init_time
|
||||||
|
ac._evtracker.set_timeout(30)
|
||||||
|
return ac, dict(configdict)
|
||||||
|
|
||||||
|
def get_online_configuring_account(self, mvbox=False, 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_watch"] = str(int(mvbox))
|
||||||
|
configdict["mvbox_move"] = str(int(move))
|
||||||
|
configdict["sentbox_watch"] = str(int(sentbox))
|
||||||
|
ac.update_config(configdict)
|
||||||
|
ac.start()
|
||||||
|
return ac
|
||||||
|
|
||||||
|
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
|
||||||
|
ac1 = self.get_online_configuring_account(
|
||||||
|
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
|
||||||
|
ac1._configtracker.wait_imap_connected()
|
||||||
|
ac1._configtracker.wait_smtp_connected()
|
||||||
|
ac1._configtracker.wait_finish()
|
||||||
|
return ac1
|
||||||
|
|
||||||
|
def get_two_online_accounts(self, move=False, quiet=False):
|
||||||
|
ac1 = self.get_online_configuring_account(move=True, quiet=quiet)
|
||||||
|
ac2 = self.get_online_configuring_account(quiet=quiet)
|
||||||
|
ac1._configtracker.wait_finish()
|
||||||
|
ac2._configtracker.wait_finish()
|
||||||
|
return ac1, ac2
|
||||||
|
|
||||||
|
def clone_online_account(self, account, pre_generated_key=True):
|
||||||
|
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._evtracker.init_time = self.init_time
|
||||||
|
ac._evtracker.set_timeout(30)
|
||||||
|
ac.update_config(dict(
|
||||||
|
addr=account.get_config("addr"),
|
||||||
|
mail_pw=account.get_config("mail_pw"),
|
||||||
|
mvbox_watch=account.get_config("mvbox_watch"),
|
||||||
|
mvbox_move=account.get_config("mvbox_move"),
|
||||||
|
sentbox_watch=account.get_config("sentbox_watch"),
|
||||||
|
))
|
||||||
|
ac.start()
|
||||||
|
return ac
|
||||||
|
|
||||||
|
def run_bot_process(self, module, ffi=True):
|
||||||
|
fn = module.__file__
|
||||||
|
|
||||||
|
bot_ac, bot_cfg = self.get_online_config()
|
||||||
|
|
||||||
|
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, bot_cfg)
|
||||||
|
self._finalizers.append(bot.kill)
|
||||||
|
return bot
|
||||||
|
|
||||||
|
am = AccountMaker()
|
||||||
|
request.addfinalizer(am.finalize)
|
||||||
|
return am
|
||||||
|
|
||||||
|
|
||||||
|
class BotProcess:
|
||||||
|
def __init__(self, popen, bot_cfg):
|
||||||
|
self.popen = popen
|
||||||
|
self.addr = bot_cfg["addr"]
|
||||||
|
|
||||||
|
# we read stdout as quickly as we can in a thread and make
|
||||||
|
# the (unicode) lines available for readers through a queue.
|
||||||
|
self.stdout_queue = queue.Queue()
|
||||||
|
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
|
||||||
|
t.setDaemon(1)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def _run_stdout_thread(self):
|
||||||
|
try:
|
||||||
|
while 1:
|
||||||
|
line = self.popen.stdout.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
line = line.strip()
|
||||||
|
self.stdout_queue.put(line)
|
||||||
|
finally:
|
||||||
|
self.stdout_queue.put(None)
|
||||||
|
|
||||||
|
def kill(self):
|
||||||
|
self.popen.kill()
|
||||||
|
|
||||||
|
def wait(self, timeout=30):
|
||||||
|
self.popen.wait(timeout=timeout)
|
||||||
|
|
||||||
|
def fnmatch_lines(self, pattern_lines):
|
||||||
|
patterns = [x.strip() for x in Source(pattern_lines.rstrip()).lines if x.strip()]
|
||||||
|
for next_pattern in patterns:
|
||||||
|
print("+++FNMATCH:", next_pattern)
|
||||||
|
ignored = []
|
||||||
|
while 1:
|
||||||
|
line = self.stdout_queue.get(timeout=15)
|
||||||
|
if line is None:
|
||||||
|
if ignored:
|
||||||
|
print("BOT stdout terminated after these lines")
|
||||||
|
for line in ignored:
|
||||||
|
print(line)
|
||||||
|
raise IOError("BOT stdout-thread terminated")
|
||||||
|
if fnmatch.fnmatch(line, next_pattern):
|
||||||
|
print("+++MATCHED:", line)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("+++IGN:", line)
|
||||||
|
ignored.append(line)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tmp_db_path(tmpdir):
|
||||||
|
return tmpdir.join("test.db").strpath
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def lp():
|
||||||
|
class Printer:
|
||||||
|
def sec(self, msg):
|
||||||
|
print()
|
||||||
|
print("=" * 10, msg, "=" * 10)
|
||||||
|
|
||||||
|
def step(self, msg):
|
||||||
|
print("-" * 5, "step " + msg, "-" * 5)
|
||||||
|
return Printer()
|
||||||
76
python/src/deltachat/tracker.py
Normal file
76
python/src/deltachat/tracker.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
from queue import Queue
|
||||||
|
from threading import Event
|
||||||
|
|
||||||
|
from .hookspec import account_hookimpl
|
||||||
|
|
||||||
|
|
||||||
|
class ImexFailed(RuntimeError):
|
||||||
|
""" Exception for signalling that import/export operations failed."""
|
||||||
|
|
||||||
|
|
||||||
|
class ImexTracker:
|
||||||
|
def __init__(self):
|
||||||
|
self._imex_events = Queue()
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
|
if ffi_event.name == "DC_EVENT_IMEX_PROGRESS":
|
||||||
|
self._imex_events.put(ffi_event.data1)
|
||||||
|
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
||||||
|
self._imex_events.put(ffi_event.data1)
|
||||||
|
|
||||||
|
def wait_finish(self, progress_timeout=60):
|
||||||
|
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||||
|
files_written = []
|
||||||
|
while True:
|
||||||
|
ev = self._imex_events.get(timeout=progress_timeout)
|
||||||
|
if isinstance(ev, str):
|
||||||
|
files_written.append(ev)
|
||||||
|
elif ev == 0:
|
||||||
|
raise ImexFailed("export failed, exp-files: {}".format(files_written))
|
||||||
|
elif ev == 1000:
|
||||||
|
return files_written
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigureFailed(RuntimeError):
|
||||||
|
""" Exception for signalling that configuration failed."""
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigureTracker:
|
||||||
|
ConfigureFailed = ConfigureFailed
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._configure_events = Queue()
|
||||||
|
self._smtp_finished = Event()
|
||||||
|
self._imap_finished = Event()
|
||||||
|
self._ffi_events = []
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_process_ffi_event(self, ffi_event):
|
||||||
|
self._ffi_events.append(ffi_event)
|
||||||
|
if ffi_event.name == "DC_EVENT_SMTP_CONNECTED":
|
||||||
|
self._smtp_finished.set()
|
||||||
|
elif ffi_event.name == "DC_EVENT_IMAP_CONNECTED":
|
||||||
|
self._imap_finished.set()
|
||||||
|
|
||||||
|
@account_hookimpl
|
||||||
|
def ac_configure_completed(self, success):
|
||||||
|
self._configure_events.put(success)
|
||||||
|
|
||||||
|
def wait_smtp_connected(self):
|
||||||
|
""" wait until smtp is configured. """
|
||||||
|
self._smtp_finished.wait()
|
||||||
|
|
||||||
|
def wait_imap_connected(self):
|
||||||
|
""" wait until smtp is configured. """
|
||||||
|
self._imap_finished.wait()
|
||||||
|
|
||||||
|
def wait_finish(self):
|
||||||
|
""" wait until configure is completed.
|
||||||
|
|
||||||
|
Raise Exception if Configure failed
|
||||||
|
"""
|
||||||
|
if not self._configure_events.get():
|
||||||
|
content = "\n".join(map(str, self._ffi_events))
|
||||||
|
raise ConfigureFailed(content)
|
||||||
@@ -1,320 +1,17 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import py
|
|
||||||
import pytest
|
|
||||||
import requests
|
|
||||||
import time
|
|
||||||
from deltachat import Account
|
|
||||||
from deltachat import const
|
|
||||||
from deltachat.capi import lib
|
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
|
||||||
parser.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(
|
|
||||||
"--ignored", action="store_true",
|
|
||||||
help="Also run tests marked with the ignored marker",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
|
||||||
config.addinivalue_line(
|
|
||||||
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
|
|
||||||
)
|
|
||||||
cfg = config.getoption('--liveconfig')
|
|
||||||
if not cfg:
|
|
||||||
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
|
|
||||||
if cfg:
|
|
||||||
config.option.liveconfig = cfg
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
|
||||||
if (list(item.iter_markers(name="ignored"))
|
|
||||||
and not item.config.getoption("ignored")):
|
|
||||||
pytest.skip("Ignored tests not requested, use --ignored")
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_header(config, startdir):
|
|
||||||
summary = []
|
|
||||||
|
|
||||||
t = tempfile.mktemp()
|
|
||||||
m = MonkeyPatch()
|
|
||||||
try:
|
|
||||||
m.setattr(sys.stdout, "write", lambda x: len(x))
|
|
||||||
ac = Account(t)
|
|
||||||
info = ac.get_info()
|
|
||||||
ac.shutdown()
|
|
||||||
finally:
|
|
||||||
m.undo()
|
|
||||||
os.remove(t)
|
|
||||||
summary.extend(['Deltachat core={} sqlite={}'.format(
|
|
||||||
info['deltachat_core_version'],
|
|
||||||
info['sqlite_version'],
|
|
||||||
)])
|
|
||||||
|
|
||||||
cfg = config.option.liveconfig
|
|
||||||
if cfg:
|
|
||||||
if "#" in cfg:
|
|
||||||
url, token = cfg.split("#", 1)
|
|
||||||
summary.append('Liveconfig provider: {}#<token ommitted>'.format(url))
|
|
||||||
else:
|
|
||||||
summary.append('Liveconfig file: {}'.format(cfg))
|
|
||||||
return summary
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def data():
|
|
||||||
class Data:
|
|
||||||
def __init__(self):
|
|
||||||
self.path = os.path.join(os.path.dirname(__file__), "data")
|
|
||||||
|
|
||||||
def get_path(self, bn):
|
|
||||||
fn = os.path.join(self.path, bn)
|
|
||||||
assert os.path.exists(fn)
|
|
||||||
return fn
|
|
||||||
return Data()
|
|
||||||
|
|
||||||
|
|
||||||
class SessionLiveConfigFromFile:
|
|
||||||
def __init__(self, fn):
|
|
||||||
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):
|
|
||||||
return self.configlist[index]
|
|
||||||
|
|
||||||
def exists(self):
|
|
||||||
return bool(self.configlist)
|
|
||||||
|
|
||||||
|
|
||||||
class SessionLiveConfigFromURL:
|
|
||||||
def __init__(self, url):
|
|
||||||
self.configlist = []
|
|
||||||
self.url = url
|
|
||||||
|
|
||||||
def get(self, index):
|
|
||||||
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 {!r}".format(res))
|
|
||||||
d = res.json()
|
|
||||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
|
||||||
self.configlist.append(config)
|
|
||||||
return config
|
|
||||||
|
|
||||||
def exists(self):
|
|
||||||
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)
|
|
||||||
else:
|
|
||||||
return SessionLiveConfigFromFile(liveconfig_opt)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
|
||||||
def datadir():
|
|
||||||
"""The py.path.local object of the test-data/ directory."""
|
|
||||||
for path in reversed(py.path.local(__file__).parts()):
|
|
||||||
datadir = path.join('test-data')
|
|
||||||
if datadir.isdir():
|
|
||||||
return datadir
|
|
||||||
else:
|
|
||||||
pytest.skip('test-data directory not found')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
|
|
||||||
|
|
||||||
class AccountMaker:
|
|
||||||
def __init__(self):
|
|
||||||
self.live_count = 0
|
|
||||||
self.offline_count = 0
|
|
||||||
self._finalizers = []
|
|
||||||
self.init_time = time.time()
|
|
||||||
self._generated_keys = ["alice", "bob", "charlie",
|
|
||||||
"dom", "elena", "fiona"]
|
|
||||||
|
|
||||||
def finalize(self):
|
|
||||||
while self._finalizers:
|
|
||||||
fin = self._finalizers.pop()
|
|
||||||
fin()
|
|
||||||
|
|
||||||
def make_account(self, path, logid):
|
|
||||||
ac = Account(path, logid=logid)
|
|
||||||
self._finalizers.append(ac.shutdown)
|
|
||||||
return ac
|
|
||||||
|
|
||||||
def get_unconfigured_account(self):
|
|
||||||
self.offline_count += 1
|
|
||||||
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
|
||||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
|
||||||
ac._evlogger.init_time = self.init_time
|
|
||||||
ac._evlogger.set_timeout(2)
|
|
||||||
return ac
|
|
||||||
|
|
||||||
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 = "key/{name}-public.asc".format(name=keyname)
|
|
||||||
fname_sec = "key/{name}-secret.asc".format(name=keyname)
|
|
||||||
account._preconfigure_keypair(addr,
|
|
||||||
datadir.join(fname_pub).read(),
|
|
||||||
datadir.join(fname_sec).read())
|
|
||||||
|
|
||||||
def get_configured_offline_account(self):
|
|
||||||
ac = self.get_unconfigured_account()
|
|
||||||
|
|
||||||
# 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 get_online_config(self, pre_generated_key=True):
|
|
||||||
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"
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
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, configdict['addr'])
|
|
||||||
ac._evlogger.init_time = self.init_time
|
|
||||||
ac._evlogger.set_timeout(30)
|
|
||||||
return ac, dict(configdict)
|
|
||||||
|
|
||||||
def get_online_configuring_account(self, mvbox=False, sentbox=False,
|
|
||||||
pre_generated_key=True, config={}):
|
|
||||||
ac, configdict = self.get_online_config(
|
|
||||||
pre_generated_key=pre_generated_key)
|
|
||||||
configdict.update(config)
|
|
||||||
ac.configure(**configdict)
|
|
||||||
return ac
|
|
||||||
|
|
||||||
def get_one_online_account(self, pre_generated_key=True):
|
|
||||||
ac1 = self.get_online_configuring_account(
|
|
||||||
pre_generated_key=pre_generated_key)
|
|
||||||
wait_successful_IMAP_SMTP_connection(ac1)
|
|
||||||
wait_configuration_progress(ac1, 1000)
|
|
||||||
ac1.start_threads()
|
|
||||||
return ac1
|
|
||||||
|
|
||||||
def get_two_online_accounts(self):
|
|
||||||
ac1 = self.get_online_configuring_account()
|
|
||||||
ac2 = self.get_online_configuring_account()
|
|
||||||
wait_successful_IMAP_SMTP_connection(ac1)
|
|
||||||
wait_configuration_progress(ac1, 1000)
|
|
||||||
ac1.start_threads()
|
|
||||||
wait_successful_IMAP_SMTP_connection(ac2)
|
|
||||||
wait_configuration_progress(ac2, 1000)
|
|
||||||
ac2.start_threads()
|
|
||||||
|
|
||||||
return ac1, ac2
|
|
||||||
|
|
||||||
def clone_online_account(self, account, pre_generated_key=True):
|
|
||||||
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._evlogger.init_time = self.init_time
|
|
||||||
ac._evlogger.set_timeout(30)
|
|
||||||
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
|
|
||||||
|
|
||||||
return ac
|
|
||||||
|
|
||||||
am = AccountMaker()
|
|
||||||
request.addfinalizer(am.finalize)
|
|
||||||
return am
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def tmp_db_path(tmpdir):
|
|
||||||
return tmpdir.join("test.db").strpath
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def lp():
|
|
||||||
class Printer:
|
|
||||||
def sec(self, msg):
|
|
||||||
print()
|
|
||||||
print("=" * 10, msg, "=" * 10)
|
|
||||||
|
|
||||||
def step(self, msg):
|
|
||||||
print("-" * 5, "step " + msg, "-" * 5)
|
|
||||||
return Printer()
|
|
||||||
|
|
||||||
|
|
||||||
def wait_configuration_progress(account, min_target, max_target=1001, check_error=True):
|
def wait_configuration_progress(account, min_target, max_target=1001, check_error=True):
|
||||||
min_target = min(min_target, max_target)
|
min_target = min(min_target, max_target)
|
||||||
while 1:
|
while 1:
|
||||||
evt_name, data1, data2 = \
|
event = account._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
||||||
account._evlogger.get_matching("DC_EVENT_CONFIGURE_PROGRESS", check_error=check_error)
|
if event.data1 >= min_target and event.data1 <= max_target:
|
||||||
if data1 >= min_target and data1 <= max_target:
|
|
||||||
print("** CONFIG PROGRESS {}".format(min_target), account)
|
print("** CONFIG PROGRESS {}".format(min_target), account)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def wait_securejoin_inviter_progress(account, target):
|
def wait_securejoin_inviter_progress(account, target):
|
||||||
while 1:
|
while 1:
|
||||||
evt_name, data1, data2 = \
|
event = account._evtracker.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
|
||||||
account._evlogger.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
|
if event.data2 >= target:
|
||||||
if data2 >= target:
|
|
||||||
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account)
|
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def wait_successful_IMAP_SMTP_connection(account):
|
|
||||||
imap_ok = smtp_ok = False
|
|
||||||
while not imap_ok or not smtp_ok:
|
|
||||||
evt_name, data1, data2 = \
|
|
||||||
account._evlogger.get_matching("DC_EVENT_(IMAP|SMTP)_CONNECTED")
|
|
||||||
if evt_name == "DC_EVENT_IMAP_CONNECTED":
|
|
||||||
imap_ok = True
|
|
||||||
print("** IMAP OK", account)
|
|
||||||
if evt_name == "DC_EVENT_SMTP_CONNECTED":
|
|
||||||
smtp_ok = True
|
|
||||||
print("** SMTP OK", account)
|
|
||||||
print("** IMAP and SMTP logins successful", account)
|
|
||||||
|
|
||||||
|
|
||||||
def wait_msgs_changed(account, chat_id, msg_id=None):
|
|
||||||
ev = account._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
|
||||||
assert ev[1] == chat_id
|
|
||||||
if msg_id is not None:
|
|
||||||
assert ev[2] == msg_id
|
|
||||||
return ev[2]
|
|
||||||
|
|||||||
1
python/tests/data/key
Symbolic link
1
python/tests/data/key
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../test-data/key
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,18 @@ import shutil
|
|||||||
import pytest
|
import pytest
|
||||||
from filecmp import cmp
|
from filecmp import cmp
|
||||||
|
|
||||||
from conftest import wait_configuration_progress, wait_msgs_changed
|
from conftest import wait_configuration_progress
|
||||||
from deltachat import const
|
from deltachat import const
|
||||||
|
|
||||||
|
|
||||||
|
def wait_msgs_changed(account, chat_id, msg_id=None):
|
||||||
|
ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
|
assert ev.data1 == chat_id
|
||||||
|
if msg_id is not None:
|
||||||
|
assert ev.data2 == msg_id
|
||||||
|
return ev.data2
|
||||||
|
|
||||||
|
|
||||||
class TestOnlineInCreation:
|
class TestOnlineInCreation:
|
||||||
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
|
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
|
||||||
ac1 = acfactory.get_online_configuring_account()
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
@@ -97,22 +105,22 @@ class TestOnlineInCreation:
|
|||||||
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
|
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
|
||||||
|
|
||||||
lp.sec("wait for the messages to be delivered to SMTP")
|
lp.sec("wait for the messages to be delivered to SMTP")
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||||
assert ev[1] == chat.id
|
assert ev.data1 == chat.id
|
||||||
assert ev[2] == prepared_original.id
|
assert ev.data2 == prepared_original.id
|
||||||
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||||
assert ev[1] == chat2.id
|
assert ev.data1 == chat2.id
|
||||||
assert ev[2] == forwarded_id
|
assert ev.data2 == forwarded_id
|
||||||
|
|
||||||
lp.sec("wait1 for original or forwarded messages to arrive")
|
lp.sec("wait1 for original or forwarded messages to arrive")
|
||||||
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
ev1 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
assert ev1[1] > const.DC_CHAT_ID_LAST_SPECIAL
|
assert ev1.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
received_original = ac2.get_message_by_id(ev1[2])
|
received_original = ac2.get_message_by_id(ev1.data2)
|
||||||
assert cmp(received_original.filename, orig, shallow=False)
|
assert cmp(received_original.filename, orig, shallow=False)
|
||||||
|
|
||||||
lp.sec("wait2 for original or forwarded messages to arrive")
|
lp.sec("wait2 for original or forwarded messages to arrive")
|
||||||
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
ev2 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
assert ev2[1] > const.DC_CHAT_ID_LAST_SPECIAL
|
assert ev2.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
assert ev2[1] != ev1[1]
|
assert ev2.data1 != ev1.data1
|
||||||
received_copy = ac2.get_message_by_id(ev2[2])
|
received_copy = ac2.get_message_by_id(ev2.data2)
|
||||||
assert cmp(received_copy.filename, orig, shallow=False)
|
assert cmp(received_copy.filename, orig, shallow=False)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback, py_dc_callback
|
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
|
||||||
|
from deltachat import register_global_plugin
|
||||||
|
from deltachat.hookspec import global_hookimpl
|
||||||
from deltachat.capi import ffi
|
from deltachat.capi import ffi
|
||||||
from deltachat.capi import lib
|
from deltachat.capi import lib
|
||||||
# from deltachat.account import EventLogger
|
# from deltachat.account import EventLogger
|
||||||
@@ -47,51 +50,37 @@ def test_callback_None2int():
|
|||||||
clear_context_callback(ctx)
|
clear_context_callback(ctx)
|
||||||
|
|
||||||
|
|
||||||
def test_start_stop_event_thread_basic():
|
def test_dc_close_events(tmpdir, acfactory):
|
||||||
print("1")
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
|
|
||||||
print("2")
|
|
||||||
ev_thread = EventThread(ctx)
|
|
||||||
print("3 -- starting event thread")
|
|
||||||
ev_thread.start()
|
|
||||||
print("4 -- stopping event thread")
|
|
||||||
ev_thread.stop()
|
|
||||||
|
|
||||||
|
# register after_shutdown function
|
||||||
|
shutdowns = []
|
||||||
|
|
||||||
# FIXME: EventLogger doesn't work without an account anymore
|
class ShutdownPlugin:
|
||||||
# def test_dc_close_events(tmpdir):
|
@global_hookimpl
|
||||||
# ctx = ffi.gc(
|
def dc_account_after_shutdown(self, account):
|
||||||
# capi.lib.dc_context_new(ffi.NULL, ffi.NULL),
|
assert account._dc_context is None
|
||||||
# lib.dc_context_unref,
|
shutdowns.append(account)
|
||||||
# )
|
register_global_plugin(ShutdownPlugin())
|
||||||
# evlog = EventLogger(ctx)
|
assert hasattr(ac1, "_dc_context")
|
||||||
# evlog.set_timeout(5)
|
ac1.shutdown()
|
||||||
# set_context_callback(
|
assert shutdowns == [ac1]
|
||||||
# ctx,
|
|
||||||
# lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2)
|
|
||||||
# )
|
|
||||||
# ev_thread = EventThread(ctx)
|
|
||||||
# ev_thread.start()
|
|
||||||
|
|
||||||
# p = tmpdir.join("hello.db")
|
def find(info_string):
|
||||||
# lib.dc_open(ctx, p.strpath.encode("ascii"), ffi.NULL)
|
evlog = ac1._evtracker
|
||||||
# capi.lib.dc_close(ctx)
|
while 1:
|
||||||
|
ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
|
||||||
|
data2 = ev.data2
|
||||||
|
if info_string in data2:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print("skipping event", ev)
|
||||||
|
|
||||||
# def find(info_string):
|
find("disconnecting inbox-thread")
|
||||||
# while 1:
|
find("disconnecting sentbox-thread")
|
||||||
# ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
|
find("disconnecting mvbox-thread")
|
||||||
# data2 = ev[2]
|
find("disconnecting SMTP")
|
||||||
# if info_string in data2:
|
find("Database closed")
|
||||||
# return
|
|
||||||
# else:
|
|
||||||
# print("skipping event", *ev)
|
|
||||||
|
|
||||||
# find("disconnecting inbox-thread")
|
|
||||||
# find("disconnecting sentbox-thread")
|
|
||||||
# find("disconnecting mvbox-thread")
|
|
||||||
# find("disconnecting SMTP")
|
|
||||||
# find("Database closed")
|
|
||||||
# ev_thread.stop()
|
|
||||||
|
|
||||||
|
|
||||||
def test_wrong_db(tmpdir):
|
def test_wrong_db(tmpdir):
|
||||||
@@ -136,10 +125,10 @@ def test_markseen_invalid_message_ids(acfactory):
|
|||||||
contact1 = ac1.create_contact(email="some1@example.com", name="some1")
|
contact1 = ac1.create_contact(email="some1@example.com", name="some1")
|
||||||
chat = ac1.create_chat_by_contact(contact1)
|
chat = ac1.create_chat_by_contact(contact1)
|
||||||
chat.send_text("one messae")
|
chat.send_text("one messae")
|
||||||
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
msg_ids = [9]
|
msg_ids = [9]
|
||||||
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
|
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
|
||||||
ac1._evlogger.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
ac1._evtracker.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
||||||
|
|
||||||
|
|
||||||
def test_get_special_message_id_returns_empty_message(acfactory):
|
def test_get_special_message_id_returns_empty_message(acfactory):
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ envlist =
|
|||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands =
|
commands =
|
||||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs:tests}
|
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs: tests examples}
|
||||||
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||||
passenv =
|
passenv =
|
||||||
TRAVIS
|
TRAVIS
|
||||||
DCC_RS_DEV
|
DCC_RS_DEV
|
||||||
DCC_RS_TARGET
|
DCC_RS_TARGET
|
||||||
DCC_PY_LIVECONFIG
|
DCC_PY_LIVECONFIG
|
||||||
DCC_NEW_TMP_EMAIL
|
DCC_NEW_TMP_EMAIL
|
||||||
CARGO_TARGET_DIR
|
CARGO_TARGET_DIR
|
||||||
RUSTC_WRAPPER
|
RUSTC_WRAPPER
|
||||||
deps =
|
deps =
|
||||||
@@ -41,13 +41,13 @@ deps =
|
|||||||
restructuredtext_lint
|
restructuredtext_lint
|
||||||
commands =
|
commands =
|
||||||
flake8 src/deltachat
|
flake8 src/deltachat
|
||||||
flake8 tests/
|
flake8 tests/ examples/
|
||||||
rst-lint --encoding 'utf-8' README.rst
|
rst-lint --encoding 'utf-8' README.rst
|
||||||
|
|
||||||
[testenv:doc]
|
[testenv:doc]
|
||||||
changedir=doc
|
changedir=doc
|
||||||
deps =
|
deps =
|
||||||
sphinx==2.2.0
|
sphinx
|
||||||
breathe
|
breathe
|
||||||
commands =
|
commands =
|
||||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||||
@@ -67,7 +67,6 @@ commands =
|
|||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
addopts = -v -ra --strict-markers
|
addopts = -v -ra --strict-markers
|
||||||
python_files = tests/test_*.py
|
|
||||||
norecursedirs = .tox
|
norecursedirs = .tox
|
||||||
xfail_strict=true
|
xfail_strict=true
|
||||||
timeout = 90
|
timeout = 90
|
||||||
|
|||||||
@@ -127,14 +127,10 @@ impl str::FromStr for Aheader {
|
|||||||
.split(';')
|
.split(';')
|
||||||
.filter_map(|a| {
|
.filter_map(|a| {
|
||||||
let attribute: Vec<&str> = a.trim().splitn(2, '=').collect();
|
let attribute: Vec<&str> = a.trim().splitn(2, '=').collect();
|
||||||
if attribute.len() < 2 {
|
match &attribute[..] {
|
||||||
return None;
|
[key, value] => Some((key.trim().to_string(), value.trim().to_string())),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((
|
|
||||||
attribute[0].trim().to_string(),
|
|
||||||
attribute[1].trim().to_string(),
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
100
src/blob.rs
100
src/blob.rs
@@ -7,13 +7,13 @@ use async_std::path::{Path, PathBuf};
|
|||||||
use async_std::prelude::*;
|
use async_std::prelude::*;
|
||||||
use async_std::{fs, io};
|
use async_std::{fs, io};
|
||||||
|
|
||||||
use self::image::GenericImageView;
|
use image::GenericImageView;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::constants::AVATAR_SIZE;
|
use crate::constants::AVATAR_SIZE;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
|
|
||||||
extern crate image;
|
|
||||||
|
|
||||||
/// Represents a file in the blob directory.
|
/// Represents a file in the blob directory.
|
||||||
///
|
///
|
||||||
/// The object has a name, which will always be valid UTF-8. Having a
|
/// The object has a name, which will always be valid UTF-8. Having a
|
||||||
@@ -58,7 +58,6 @@ impl<'a> BlobObject<'a> {
|
|||||||
blobdir: blobdir.to_path_buf(),
|
blobdir: blobdir.to_path_buf(),
|
||||||
blobname: name.clone(),
|
blobname: name.clone(),
|
||||||
cause: err,
|
cause: err,
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
})?;
|
||||||
let blob = BlobObject {
|
let blob = BlobObject {
|
||||||
blobdir,
|
blobdir,
|
||||||
@@ -91,7 +90,6 @@ impl<'a> BlobObject<'a> {
|
|||||||
blobdir: dir.to_path_buf(),
|
blobdir: dir.to_path_buf(),
|
||||||
blobname: name,
|
blobname: name,
|
||||||
cause: err,
|
cause: err,
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
||||||
@@ -104,7 +102,6 @@ impl<'a> BlobObject<'a> {
|
|||||||
blobdir: dir.to_path_buf(),
|
blobdir: dir.to_path_buf(),
|
||||||
blobname: name,
|
blobname: name,
|
||||||
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
|
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +129,6 @@ impl<'a> BlobObject<'a> {
|
|||||||
blobname: String::from(""),
|
blobname: String::from(""),
|
||||||
src: src.as_ref().to_path_buf(),
|
src: src.as_ref().to_path_buf(),
|
||||||
cause: err,
|
cause: err,
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
})?;
|
||||||
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
|
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
|
||||||
let (name, mut dst_file) =
|
let (name, mut dst_file) =
|
||||||
@@ -149,7 +145,6 @@ impl<'a> BlobObject<'a> {
|
|||||||
blobname: name_for_err,
|
blobname: name_for_err,
|
||||||
src: src.as_ref().to_path_buf(),
|
src: src.as_ref().to_path_buf(),
|
||||||
cause: err,
|
cause: err,
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let blob = BlobObject {
|
let blob = BlobObject {
|
||||||
@@ -209,17 +204,14 @@ impl<'a> BlobObject<'a> {
|
|||||||
.map_err(|_| BlobError::WrongBlobdir {
|
.map_err(|_| BlobError::WrongBlobdir {
|
||||||
blobdir: context.get_blobdir().to_path_buf(),
|
blobdir: context.get_blobdir().to_path_buf(),
|
||||||
src: path.as_ref().to_path_buf(),
|
src: path.as_ref().to_path_buf(),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
})?;
|
||||||
if !BlobObject::is_acceptible_blob_name(&rel_path) {
|
if !BlobObject::is_acceptible_blob_name(&rel_path) {
|
||||||
return Err(BlobError::WrongName {
|
return Err(BlobError::WrongName {
|
||||||
blobname: path.as_ref().to_path_buf(),
|
blobname: path.as_ref().to_path_buf(),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
|
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
|
||||||
blobname: path.as_ref().to_path_buf(),
|
blobname: path.as_ref().to_path_buf(),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
})?;
|
||||||
BlobObject::from_name(context, name.to_string())
|
BlobObject::from_name(context, name.to_string())
|
||||||
}
|
}
|
||||||
@@ -247,7 +239,6 @@ impl<'a> BlobObject<'a> {
|
|||||||
if !BlobObject::is_acceptible_blob_name(&name) {
|
if !BlobObject::is_acceptible_blob_name(&name) {
|
||||||
return Err(BlobError::WrongName {
|
return Err(BlobError::WrongName {
|
||||||
blobname: PathBuf::from(name),
|
blobname: PathBuf::from(name),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(BlobObject {
|
Ok(BlobObject {
|
||||||
@@ -370,7 +361,6 @@ impl<'a> BlobObject<'a> {
|
|||||||
blobdir: context.get_blobdir().to_path_buf(),
|
blobdir: context.get_blobdir().to_path_buf(),
|
||||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||||
cause: err,
|
cause: err,
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
|
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
|
||||||
@@ -383,7 +373,6 @@ impl<'a> BlobObject<'a> {
|
|||||||
blobdir: context.get_blobdir().to_path_buf(),
|
blobdir: context.get_blobdir().to_path_buf(),
|
||||||
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
|
||||||
cause: err,
|
cause: err,
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -397,98 +386,41 @@ impl<'a> fmt::Display for BlobObject<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Errors for the [BlobObject].
|
/// Errors for the [BlobObject].
|
||||||
#[derive(Fail, Debug)]
|
#[derive(Debug, Error)]
|
||||||
pub enum BlobError {
|
pub enum BlobError {
|
||||||
|
#[error("Failed to create blob {blobname} in {}", .blobdir.display())]
|
||||||
CreateFailure {
|
CreateFailure {
|
||||||
blobdir: PathBuf,
|
blobdir: PathBuf,
|
||||||
blobname: String,
|
blobname: String,
|
||||||
#[cause]
|
#[source]
|
||||||
cause: std::io::Error,
|
cause: std::io::Error,
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
},
|
||||||
|
#[error("Failed to write data to blob {blobname} in {}", .blobdir.display())]
|
||||||
WriteFailure {
|
WriteFailure {
|
||||||
blobdir: PathBuf,
|
blobdir: PathBuf,
|
||||||
blobname: String,
|
blobname: String,
|
||||||
#[cause]
|
#[source]
|
||||||
cause: std::io::Error,
|
cause: std::io::Error,
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
},
|
||||||
|
#[error("Failed to copy data from {} to blob {blobname} in {}", .src.display(), .blobdir.display())]
|
||||||
CopyFailure {
|
CopyFailure {
|
||||||
blobdir: PathBuf,
|
blobdir: PathBuf,
|
||||||
blobname: String,
|
blobname: String,
|
||||||
src: PathBuf,
|
src: PathBuf,
|
||||||
#[cause]
|
#[source]
|
||||||
cause: std::io::Error,
|
cause: std::io::Error,
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
},
|
||||||
|
#[error("Failed to recode to blob {blobname} in {}", .blobdir.display())]
|
||||||
RecodeFailure {
|
RecodeFailure {
|
||||||
blobdir: PathBuf,
|
blobdir: PathBuf,
|
||||||
blobname: String,
|
blobname: String,
|
||||||
#[cause]
|
#[source]
|
||||||
cause: image::ImageError,
|
cause: image::ImageError,
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
},
|
||||||
WrongBlobdir {
|
#[error("File path {} is not in {}", .src.display(), .blobdir.display())]
|
||||||
blobdir: PathBuf,
|
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
|
||||||
src: PathBuf,
|
#[error("Blob has a badname {}", .blobname.display())]
|
||||||
backtrace: failure::Backtrace,
|
WrongName { blobname: PathBuf },
|
||||||
},
|
|
||||||
WrongName {
|
|
||||||
blobname: PathBuf,
|
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementing Display is done by hand because the failure
|
|
||||||
// #[fail(display = "...")] syntax does not allow using
|
|
||||||
// `blobdir.display()`.
|
|
||||||
impl fmt::Display for BlobError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
// Match on the data rather than kind, they are equivalent for
|
|
||||||
// identifying purposes but contain the actual data we need.
|
|
||||||
match &self {
|
|
||||||
BlobError::CreateFailure {
|
|
||||||
blobdir, blobname, ..
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"Failed to create blob {} in {}",
|
|
||||||
blobname,
|
|
||||||
blobdir.display()
|
|
||||||
),
|
|
||||||
BlobError::WriteFailure {
|
|
||||||
blobdir, blobname, ..
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"Failed to write data to blob {} in {}",
|
|
||||||
blobname,
|
|
||||||
blobdir.display()
|
|
||||||
),
|
|
||||||
BlobError::CopyFailure {
|
|
||||||
blobdir,
|
|
||||||
blobname,
|
|
||||||
src,
|
|
||||||
..
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"Failed to copy data from {} to blob {} in {}",
|
|
||||||
src.display(),
|
|
||||||
blobname,
|
|
||||||
blobdir.display(),
|
|
||||||
),
|
|
||||||
BlobError::RecodeFailure {
|
|
||||||
blobdir, blobname, ..
|
|
||||||
} => write!(f, "Failed to recode {} in {}", blobname, blobdir.display(),),
|
|
||||||
BlobError::WrongBlobdir { blobdir, src, .. } => write!(
|
|
||||||
f,
|
|
||||||
"File path {} is not in blobdir {}",
|
|
||||||
src.display(),
|
|
||||||
blobdir.display(),
|
|
||||||
),
|
|
||||||
BlobError::WrongName { blobname, .. } => {
|
|
||||||
write!(f, "Blob has a bad name: {}", blobname.display(),)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
225
src/chat.rs
225
src/chat.rs
@@ -15,7 +15,7 @@ use crate::constants::*;
|
|||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::error::Error;
|
use crate::error::{bail, ensure, format_err, Error};
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
use crate::job::{self, Action};
|
use crate::job::{self, Action};
|
||||||
use crate::message::{self, InvalidMsgId, Message, MessageState, MsgId};
|
use crate::message::{self, InvalidMsgId, Message, MessageState, MsgId};
|
||||||
@@ -340,13 +340,12 @@ impl ChatId {
|
|||||||
time(),
|
time(),
|
||||||
msg.viewtype,
|
msg.viewtype,
|
||||||
MessageState::OutDraft,
|
MessageState::OutDraft,
|
||||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
msg.text.as_deref().unwrap_or(""),
|
||||||
msg.param.to_string(),
|
msg.param.to_string(),
|
||||||
1,
|
1,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +398,62 @@ impl ChatId {
|
|||||||
Ok(self.get_param(context).await?.exists(Param::Devicetalk))
|
Ok(self.get_param(context).await?.exists(Param::Devicetalk))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn parent_query<T, F>(
|
||||||
|
self,
|
||||||
|
context: &Context,
|
||||||
|
fields: &str,
|
||||||
|
f: F,
|
||||||
|
) -> sql::Result<Option<T>>
|
||||||
|
where
|
||||||
|
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||||
|
{
|
||||||
|
let sql = &context.sql;
|
||||||
|
let query = format!(
|
||||||
|
"SELECT {} \
|
||||||
|
FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \
|
||||||
|
ORDER BY timestamp DESC, id DESC \
|
||||||
|
LIMIT 1;",
|
||||||
|
fields
|
||||||
|
);
|
||||||
|
sql.query_row_optional(
|
||||||
|
query,
|
||||||
|
paramsv![
|
||||||
|
self,
|
||||||
|
MessageState::OutPreparing,
|
||||||
|
MessageState::OutDraft,
|
||||||
|
MessageState::OutPending,
|
||||||
|
MessageState::OutFailed
|
||||||
|
],
|
||||||
|
f,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_parent_mime_headers(self, context: &Context) -> Option<(String, String, String)> {
|
||||||
|
let collect = |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?, row.get(2)?));
|
||||||
|
self.parent_query(
|
||||||
|
context,
|
||||||
|
"rfc724_mid, mime_in_reply_to, mime_references",
|
||||||
|
collect,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parent_is_encrypted(self, context: &Context) -> Result<bool, Error> {
|
||||||
|
let collect = |row: &rusqlite::Row| Ok(row.get(0)?);
|
||||||
|
let packed: Option<String> = self.parent_query(context, "param", collect).await?;
|
||||||
|
|
||||||
|
if let Some(ref packed) = packed {
|
||||||
|
let param = packed.parse::<Params>()?;
|
||||||
|
Ok(param.exists(Param::GuaranteeE2ee))
|
||||||
|
} else {
|
||||||
|
// No messages
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Bad evil escape hatch.
|
/// Bad evil escape hatch.
|
||||||
///
|
///
|
||||||
/// Avoid using this, eventually types should be cleaned up enough
|
/// Avoid using this, eventually types should be cleaned up enough
|
||||||
@@ -580,44 +635,6 @@ impl Chat {
|
|||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_subtitle(&self, context: &Context) -> String {
|
|
||||||
// returns either the address or the number of chat members
|
|
||||||
|
|
||||||
if self.typ == Chattype::Single && self.param.exists(Param::Selftalk) {
|
|
||||||
return context
|
|
||||||
.stock_str(StockMessage::SelfTalkSubTitle)
|
|
||||||
.await
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.typ == Chattype::Single {
|
|
||||||
return context
|
|
||||||
.sql
|
|
||||||
.query_get_value(
|
|
||||||
context,
|
|
||||||
"SELECT c.addr
|
|
||||||
FROM chats_contacts cc
|
|
||||||
LEFT JOIN contacts c ON c.id=cc.contact_id
|
|
||||||
WHERE cc.chat_id=?;",
|
|
||||||
paramsv![self.id],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|| "Err".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup {
|
|
||||||
if self.id.is_deaddrop() {
|
|
||||||
return context.stock_str(StockMessage::DeadDrop).await.into();
|
|
||||||
}
|
|
||||||
let cnt = get_chat_contact_cnt(context, self.id).await;
|
|
||||||
return context
|
|
||||||
.stock_string_repl_int(StockMessage::Member, cnt as i32)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
"Err".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parent_query(fields: &str) -> String {
|
fn parent_query(fields: &str) -> String {
|
||||||
// Check for server_uid guarantees that we don't
|
// Check for server_uid guarantees that we don't
|
||||||
// select a draft or undelivered message.
|
// select a draft or undelivered message.
|
||||||
@@ -717,7 +734,6 @@ impl Chat {
|
|||||||
.await
|
.await
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.unwrap_or_else(std::path::PathBuf::new),
|
.unwrap_or_else(std::path::PathBuf::new),
|
||||||
subtitle: self.get_subtitle(context).await,
|
|
||||||
draft,
|
draft,
|
||||||
is_muted: self.is_muted(),
|
is_muted: self.is_muted(),
|
||||||
})
|
})
|
||||||
@@ -873,7 +889,7 @@ impl Chat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if can_encrypt && (all_mutual || self.parent_is_encrypted(context).await?) {
|
if can_encrypt && (all_mutual || self.id.parent_is_encrypted(context).await?) {
|
||||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -888,7 +904,7 @@ impl Chat {
|
|||||||
// we do not set In-Reply-To/References in this case.
|
// we do not set In-Reply-To/References in this case.
|
||||||
if !self.is_self_talk() {
|
if !self.is_self_talk() {
|
||||||
if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
|
if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
|
||||||
self.get_parent_mime_headers(context).await
|
self.id.get_parent_mime_headers(context).await
|
||||||
{
|
{
|
||||||
if !parent_rfc724_mid.is_empty() {
|
if !parent_rfc724_mid.is_empty() {
|
||||||
new_in_reply_to = parent_rfc724_mid.clone();
|
new_in_reply_to = parent_rfc724_mid.clone();
|
||||||
@@ -1067,9 +1083,6 @@ pub struct ChatInfo {
|
|||||||
/// currently.
|
/// currently.
|
||||||
pub profile_image: std::path::PathBuf,
|
pub profile_image: std::path::PathBuf,
|
||||||
|
|
||||||
/// Subtitle for the chat.
|
|
||||||
pub subtitle: String,
|
|
||||||
|
|
||||||
/// The draft message text.
|
/// The draft message text.
|
||||||
///
|
///
|
||||||
/// If the chat has not draft this is an empty string.
|
/// If the chat has not draft this is an empty string.
|
||||||
@@ -1535,7 +1548,7 @@ pub async fn get_chat_msgs(
|
|||||||
flags: u32,
|
flags: u32,
|
||||||
marker1before: Option<MsgId>,
|
marker1before: Option<MsgId>,
|
||||||
) -> Vec<MsgId> {
|
) -> Vec<MsgId> {
|
||||||
match hide_device_expired_messages(context).await {
|
match delete_device_expired_messages(context).await {
|
||||||
Err(err) => warn!(context, "Failed to delete expired messages: {}", err),
|
Err(err) => warn!(context, "Failed to delete expired messages: {}", err),
|
||||||
Ok(messages_deleted) => {
|
Ok(messages_deleted) => {
|
||||||
if messages_deleted {
|
if messages_deleted {
|
||||||
@@ -1700,11 +1713,11 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hides messages which are expired according to "delete_device_after" setting.
|
/// Deletes messages which are expired according to "delete_device_after" setting.
|
||||||
///
|
///
|
||||||
/// Returns true if any message is hidden, so event can be emitted. If nothing
|
/// Returns true if any message is deleted, so event can be emitted. If nothing
|
||||||
/// has been hidden, returns false.
|
/// has been deleted, returns false.
|
||||||
pub async fn hide_device_expired_messages(context: &Context) -> Result<bool, Error> {
|
pub async fn delete_device_expired_messages(context: &Context) -> Result<bool, Error> {
|
||||||
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
|
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
|
||||||
let threshold_timestamp = time() - delete_device_after;
|
let threshold_timestamp = time() - delete_device_after;
|
||||||
|
|
||||||
@@ -1717,7 +1730,7 @@ pub async fn hide_device_expired_messages(context: &Context) -> Result<bool, Err
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.0;
|
.0;
|
||||||
|
|
||||||
// Hide expired messages
|
// Delete expired messages
|
||||||
//
|
//
|
||||||
// Only update the rows that have to be updated, to avoid emitting
|
// Only update the rows that have to be updated, to avoid emitting
|
||||||
// unnecessary "chat modified" events.
|
// unnecessary "chat modified" events.
|
||||||
@@ -1725,13 +1738,14 @@ pub async fn hide_device_expired_messages(context: &Context) -> Result<bool, Err
|
|||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
"UPDATE msgs \
|
"UPDATE msgs \
|
||||||
SET txt = 'DELETED', hidden = 1 \
|
SET txt = 'DELETED', chat_id = ? \
|
||||||
WHERE timestamp < ? \
|
WHERE timestamp < ? \
|
||||||
AND chat_id > ? \
|
AND chat_id > ? \
|
||||||
AND chat_id != ? \
|
AND chat_id != ? \
|
||||||
AND chat_id != ? \
|
AND chat_id != ? \
|
||||||
AND NOT hidden",
|
AND NOT hidden",
|
||||||
paramsv![
|
paramsv![
|
||||||
|
DC_CHAT_ID_TRASH,
|
||||||
threshold_timestamp,
|
threshold_timestamp,
|
||||||
DC_CHAT_ID_LAST_SPECIAL,
|
DC_CHAT_ID_LAST_SPECIAL,
|
||||||
self_chat_id,
|
self_chat_id,
|
||||||
@@ -1822,17 +1836,17 @@ pub async fn get_next_media(
|
|||||||
msg_type3,
|
msg_type3,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
for i in 0..list.len() {
|
for (i, msg_id) in list.iter().enumerate() {
|
||||||
if curr_msg_id == list[i] {
|
if curr_msg_id == *msg_id {
|
||||||
match direction {
|
match direction {
|
||||||
Direction::Forward => {
|
Direction::Forward => {
|
||||||
if i + 1 < list.len() {
|
if i + 1 < list.len() {
|
||||||
ret = Some(list[i + 1]);
|
ret = list.get(i + 1).copied();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Direction::Backward => {
|
Direction::Backward => {
|
||||||
if i >= 1 {
|
if i >= 1 {
|
||||||
ret = Some(list[i - 1]);
|
ret = list.get(i - 1).copied();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1918,38 +1932,56 @@ pub async fn create_group_chat(
|
|||||||
Ok(chat_id)
|
Ok(chat_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// add a contact to the chats_contact table
|
||||||
pub(crate) async fn add_to_chat_contacts_table(
|
pub(crate) async fn add_to_chat_contacts_table(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
chat_id: ChatId,
|
chat_id: ChatId,
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// add a contact to a chat; the function does not check the type or if any of the record exist or are already
|
match context
|
||||||
// added to the chat!
|
|
||||||
context
|
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||||
paramsv![chat_id, contact_id as i32],
|
paramsv![chat_id, contact_id as i32],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
{
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
context,
|
||||||
|
"could not add {} to chat {} table: {}", contact_id, chat_id, err
|
||||||
|
);
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// remove a contact from the chats_contact table
|
||||||
pub(crate) async fn remove_from_chat_contacts_table(
|
pub(crate) async fn remove_from_chat_contacts_table(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
chat_id: ChatId,
|
chat_id: ChatId,
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// remove a contact from the chats_contact table unconditionally
|
match context
|
||||||
// the function does not check the type or if the record exist
|
|
||||||
context
|
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
|
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
|
||||||
paramsv![chat_id, contact_id as i32],
|
paramsv![chat_id, contact_id as i32],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
{
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(_) => {
|
||||||
|
warn!(
|
||||||
|
context,
|
||||||
|
"could not remove contact {:?} from chat {:?}", contact_id, chat_id
|
||||||
|
);
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a contact to the chat.
|
/// Adds a contact to the chat.
|
||||||
@@ -2049,15 +2081,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
|||||||
msg.param.set(Param::Arg, contact.get_addr());
|
msg.param.set(Param::Arg, contact.get_addr());
|
||||||
msg.param.set_int(Param::Arg2, from_handshake.into());
|
msg.param.set_int(Param::Arg2, from_handshake.into());
|
||||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||||
context.call_cb(Event::MsgsChanged {
|
|
||||||
chat_id,
|
|
||||||
msg_id: msg.id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
context.call_cb(Event::MsgsChanged {
|
|
||||||
chat_id,
|
|
||||||
msg_id: MsgId::new(0),
|
|
||||||
});
|
|
||||||
context.call_cb(Event::ChatModified(chat_id));
|
context.call_cb(Event::ChatModified(chat_id));
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
@@ -2211,19 +2235,18 @@ pub async fn set_muted(
|
|||||||
duration: MuteDuration,
|
duration: MuteDuration,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||||
if real_group_exists(context, chat_id).await
|
if context
|
||||||
&& context
|
.sql
|
||||||
.sql
|
.execute(
|
||||||
.execute(
|
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
paramsv![duration, chat_id],
|
||||||
paramsv![duration, chat_id],
|
)
|
||||||
)
|
.await
|
||||||
.await
|
.is_ok()
|
||||||
.is_ok()
|
|
||||||
{
|
{
|
||||||
context.call_cb(Event::ChatModified(chat_id));
|
context.call_cb(Event::ChatModified(chat_id));
|
||||||
} else {
|
} else {
|
||||||
bail!("Failed to set name");
|
bail!("Failed to set mute duration, chat might not exist -");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2257,8 +2280,7 @@ pub async fn remove_contact_from_chat(
|
|||||||
"Cannot remove contact from chat; self not in group.".into()
|
"Cannot remove contact from chat; self not in group.".into()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else if remove_from_chat_contacts_table(context, chat_id, contact_id).await {
|
} else {
|
||||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
|
||||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||||
if chat.is_promoted() {
|
if chat.is_promoted() {
|
||||||
msg.viewtype = Viewtype::Text;
|
msg.viewtype = Viewtype::Text;
|
||||||
@@ -2289,14 +2311,20 @@ pub async fn remove_contact_from_chat(
|
|||||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||||
msg.param.set(Param::Arg, contact.get_addr());
|
msg.param.set(Param::Arg, contact.get_addr());
|
||||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||||
context.call_cb(Event::MsgsChanged {
|
|
||||||
chat_id,
|
|
||||||
msg_id: msg.id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// we remove the member from the chat after constructing the
|
||||||
|
// to-be-send message. If between send_msg() and here the
|
||||||
|
// process dies the user will have to re-do the action. It's
|
||||||
|
// better than the other way round: you removed
|
||||||
|
// someone from DB but no peer or device gets to know about it and
|
||||||
|
// group membership is thus different on different devices.
|
||||||
|
// Note also that sending a message needs all recipients
|
||||||
|
// in order to correctly determine encryption so if we
|
||||||
|
// removed it first, it would complicate the
|
||||||
|
// check/encryption logic.
|
||||||
|
success = remove_from_chat_contacts_table(context, chat_id, contact_id).await;
|
||||||
context.call_cb(Event::ChatModified(chat_id));
|
context.call_cb(Event::ChatModified(chat_id));
|
||||||
success = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2781,7 +2809,6 @@ mod tests {
|
|||||||
"is_sending_locations": false,
|
"is_sending_locations": false,
|
||||||
"color": 15895624,
|
"color": 15895624,
|
||||||
"profile_image": "",
|
"profile_image": "",
|
||||||
"subtitle": "bob@example.com",
|
|
||||||
"draft": "",
|
"draft": "",
|
||||||
"is_muted": false
|
"is_muted": false
|
||||||
}
|
}
|
||||||
@@ -3402,4 +3429,18 @@ mod tests {
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_parent_is_encrypted() {
|
||||||
|
let t = dummy_context().await;
|
||||||
|
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(!chat_id.parent_is_encrypted(&t.ctx).await.unwrap());
|
||||||
|
|
||||||
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
|
msg.set_text(Some("hello".to_string()));
|
||||||
|
chat_id.set_draft(&t.ctx, Some(&mut msg)).await;
|
||||||
|
assert!(!chat_id.parent_is_encrypted(&t.ctx).await.unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::chat::*;
|
|||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
use crate::context::*;
|
use crate::context::*;
|
||||||
use crate::error::Result;
|
use crate::error::{bail, ensure, Result};
|
||||||
use crate::lot::Lot;
|
use crate::lot::Lot;
|
||||||
use crate::message::{Message, MessageState, MsgId};
|
use crate::message::{Message, MessageState, MsgId};
|
||||||
use crate::stock::StockMessage;
|
use crate::stock::StockMessage;
|
||||||
@@ -92,9 +92,14 @@ impl Chatlist {
|
|||||||
query: Option<&str>,
|
query: Option<&str>,
|
||||||
query_contact_id: Option<u32>,
|
query_contact_id: Option<u32>,
|
||||||
) -> Result<Self> {
|
) -> 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
|
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
|
||||||
// messages get hidden to avoid reloading the same chatlist.
|
// messages get deleted to avoid reloading the same chatlist.
|
||||||
if let Err(err) = hide_device_expired_messages(context).await {
|
if let Err(err) = delete_device_expired_messages(context).await {
|
||||||
warn!(context, "Failed to hide expired messages: {}", err);
|
warn!(context, "Failed to hide expired messages: {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +116,7 @@ impl Chatlist {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
};
|
};
|
||||||
|
|
||||||
let skip_id = if 0 != listflags & DC_GCL_FOR_FORWARDING {
|
let skip_id = if flag_for_forwarding {
|
||||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -156,7 +161,7 @@ impl Chatlist {
|
|||||||
process_row,
|
process_row,
|
||||||
process_rows,
|
process_rows,
|
||||||
).await?
|
).await?
|
||||||
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
|
} else if flag_archived_only {
|
||||||
// show archived chats
|
// show archived chats
|
||||||
// (this includes the archived device-chat; we could skip it,
|
// (this includes the archived device-chat; we could skip it,
|
||||||
// however, then the number of archived chats do not match, which might be even more irritating.
|
// however, then the number of archived chats do not match, which might be even more irritating.
|
||||||
@@ -218,7 +223,7 @@ impl Chatlist {
|
|||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
// show normal chatlist
|
// show normal chatlist
|
||||||
let sort_id_up = if 0 != listflags & DC_GCL_FOR_FORWARDING {
|
let sort_id_up = if flag_for_forwarding {
|
||||||
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -245,10 +250,10 @@ impl Chatlist {
|
|||||||
process_row,
|
process_row,
|
||||||
process_rows,
|
process_rows,
|
||||||
).await?;
|
).await?;
|
||||||
if 0 == listflags & DC_GCL_NO_SPECIALS {
|
if !flag_no_specials {
|
||||||
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
|
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
|
||||||
{
|
{
|
||||||
if 0 == listflags & DC_GCL_FOR_FORWARDING {
|
if !flag_for_forwarding {
|
||||||
ids.insert(
|
ids.insert(
|
||||||
0,
|
0,
|
||||||
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
|
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
|
||||||
@@ -261,7 +266,7 @@ impl Chatlist {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if add_archived_link_item && dc_get_archived_cnt(context).await > 0 {
|
if add_archived_link_item && dc_get_archived_cnt(context).await > 0 {
|
||||||
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
|
if ids.is_empty() && flag_add_alldone_hint {
|
||||||
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
|
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
|
||||||
}
|
}
|
||||||
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
|
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
|
||||||
@@ -284,18 +289,20 @@ impl Chatlist {
|
|||||||
///
|
///
|
||||||
/// To get the message object from the message ID, use dc_get_chat().
|
/// To get the message object from the message ID, use dc_get_chat().
|
||||||
pub fn get_chat_id(&self, index: usize) -> ChatId {
|
pub fn get_chat_id(&self, index: usize) -> ChatId {
|
||||||
if index >= self.ids.len() {
|
match self.ids.get(index) {
|
||||||
return ChatId::new(0);
|
Some((chat_id, _msg_id)) => *chat_id,
|
||||||
|
None => ChatId::new(0),
|
||||||
}
|
}
|
||||||
self.ids[index].0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a single message ID of a chatlist.
|
/// Get a single message ID of a chatlist.
|
||||||
///
|
///
|
||||||
/// To get the message object from the message ID, use dc_get_msg().
|
/// To get the message object from the message ID, use dc_get_msg().
|
||||||
pub fn get_msg_id(&self, index: usize) -> Result<MsgId> {
|
pub fn get_msg_id(&self, index: usize) -> Result<MsgId> {
|
||||||
ensure!(index < self.ids.len(), "Chatlist index out of range");
|
match self.ids.get(index) {
|
||||||
Ok(self.ids[index].1)
|
Some((_chat_id, msg_id)) => Ok(*msg_id),
|
||||||
|
None => bail!("Chatlist index out of range"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a summary for a chatlist index.
|
/// Get a summary for a chatlist index.
|
||||||
@@ -319,25 +326,27 @@ impl Chatlist {
|
|||||||
// Also, sth. as "No messages" would not work if the summary comes from a message.
|
// Also, sth. as "No messages" would not work if the summary comes from a message.
|
||||||
let mut ret = Lot::new();
|
let mut ret = Lot::new();
|
||||||
|
|
||||||
if index >= self.ids.len() {
|
let (chat_id, lastmsg_id) = match self.ids.get(index) {
|
||||||
ret.text2 = Some("ErrBadChatlistIndex".to_string());
|
Some(ids) => ids,
|
||||||
return ret;
|
None => {
|
||||||
}
|
ret.text2 = Some("ErrBadChatlistIndex".to_string());
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let chat_loaded: Chat;
|
let chat_loaded: Chat;
|
||||||
let chat = if let Some(chat) = chat {
|
let chat = if let Some(chat) = chat {
|
||||||
chat
|
chat
|
||||||
} else if let Ok(chat) = Chat::load_from_db(context, self.ids[index].0).await {
|
} else if let Ok(chat) = Chat::load_from_db(context, *chat_id).await {
|
||||||
chat_loaded = chat;
|
chat_loaded = chat;
|
||||||
&chat_loaded
|
&chat_loaded
|
||||||
} else {
|
} else {
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
let lastmsg_id = self.ids[index].1;
|
|
||||||
let mut lastcontact = None;
|
let mut lastcontact = None;
|
||||||
|
|
||||||
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
|
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id).await {
|
||||||
if lastmsg.from_id != DC_CONTACT_ID_SELF
|
if lastmsg.from_id != DC_CONTACT_ID_SELF
|
||||||
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
|
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ pub enum Config {
|
|||||||
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
|
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
|
||||||
ShowEmails,
|
ShowEmails,
|
||||||
|
|
||||||
|
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||||
|
MediaQuality,
|
||||||
|
|
||||||
#[strum(props(default = "0"))]
|
#[strum(props(default = "0"))]
|
||||||
KeyGenType,
|
KeyGenType,
|
||||||
|
|
||||||
@@ -251,9 +254,11 @@ mod tests {
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
|
||||||
|
use crate::constants;
|
||||||
use crate::constants::AVATAR_SIZE;
|
use crate::constants::AVATAR_SIZE;
|
||||||
use crate::test_utils::*;
|
use crate::test_utils::*;
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
|
use num_traits::FromPrimitive;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
@@ -328,4 +333,48 @@ mod tests {
|
|||||||
assert_eq!(img.width(), AVATAR_SIZE);
|
assert_eq!(img.width(), AVATAR_SIZE);
|
||||||
assert_eq!(img.height(), AVATAR_SIZE);
|
assert_eq!(img.height(), AVATAR_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_selfavatar_copy_without_recode() {
|
||||||
|
let t = dummy_context().await;
|
||||||
|
let avatar_src = t.dir.path().join("avatar.png");
|
||||||
|
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||||
|
File::create(&avatar_src)
|
||||||
|
.unwrap()
|
||||||
|
.write_all(avatar_bytes)
|
||||||
|
.unwrap();
|
||||||
|
let avatar_blob = t.ctx.get_blobdir().join("avatar.png");
|
||||||
|
assert!(!avatar_blob.exists().await);
|
||||||
|
t.ctx
|
||||||
|
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(avatar_blob.exists().await);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::metadata(&avatar_blob).unwrap().len(),
|
||||||
|
avatar_bytes.len() as u64
|
||||||
|
);
|
||||||
|
let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
|
||||||
|
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_media_quality_config_option() {
|
||||||
|
let t = dummy_context().await;
|
||||||
|
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
|
||||||
|
assert_eq!(media_quality, 0);
|
||||||
|
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||||
|
assert_eq!(media_quality, constants::MediaQuality::Balanced);
|
||||||
|
|
||||||
|
t.ctx
|
||||||
|
.set_config(Config::MediaQuality, Some("1"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
|
||||||
|
assert_eq!(media_quality, 1);
|
||||||
|
assert_eq!(constants::MediaQuality::Worse as i32, 1);
|
||||||
|
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
|
||||||
|
assert_eq!(media_quality, constants::MediaQuality::Worse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
//! # Thunderbird's Autoconfiguration implementation
|
//! # Thunderbird's Autoconfiguration implementation
|
||||||
//!
|
//!
|
||||||
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
|
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
|
||||||
use quick_xml;
|
|
||||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||||
|
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
@@ -9,33 +8,7 @@ use crate::context::Context;
|
|||||||
use crate::login_param::LoginParam;
|
use crate::login_param::LoginParam;
|
||||||
|
|
||||||
use super::read_url::read_url;
|
use super::read_url::read_url;
|
||||||
|
use super::Error;
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum Error {
|
|
||||||
#[fail(display = "Invalid email address: {:?}", _0)]
|
|
||||||
InvalidEmailAddress(String),
|
|
||||||
|
|
||||||
#[fail(display = "XML error at position {}", position)]
|
|
||||||
InvalidXml {
|
|
||||||
position: usize,
|
|
||||||
#[cause]
|
|
||||||
error: quick_xml::Error,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[fail(display = "Bad or incomplete autoconfig")]
|
|
||||||
IncompleteAutoconfig(LoginParam),
|
|
||||||
|
|
||||||
#[fail(display = "Failed to get URL {}", _0)]
|
|
||||||
ReadUrlError(#[cause] super::read_url::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
impl From<super::read_url::Error> for Error {
|
|
||||||
fn from(err: super::read_url::Error) -> Error {
|
|
||||||
Error::ReadUrlError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct MozAutoconfigure<'a> {
|
struct MozAutoconfigure<'a> {
|
||||||
@@ -65,16 +38,16 @@ enum MozConfigTag {
|
|||||||
Username,
|
Username,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
|
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
|
||||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
||||||
reader.trim_text(true);
|
reader.trim_text(true);
|
||||||
|
|
||||||
// Split address into local part and domain part.
|
// Split address into local part and domain part.
|
||||||
let p = in_emailaddr
|
let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect();
|
||||||
.find('@')
|
let (in_emaillocalpart, in_emaildomain) = match &parts[..] {
|
||||||
.ok_or_else(|| Error::InvalidEmailAddress(in_emailaddr.to_string()))?;
|
[domain, local] => (local, domain),
|
||||||
let (in_emaillocalpart, in_emaildomain) = in_emailaddr.split_at(p);
|
_ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())),
|
||||||
let in_emaildomain = &in_emaildomain[1..];
|
};
|
||||||
|
|
||||||
let mut moz_ac = MozAutoconfigure {
|
let mut moz_ac = MozAutoconfigure {
|
||||||
in_emailaddr,
|
in_emailaddr,
|
||||||
@@ -125,7 +98,7 @@ pub fn moz_autoconfigure(
|
|||||||
context: &Context,
|
context: &Context,
|
||||||
url: &str,
|
url: &str,
|
||||||
param_in: &LoginParam,
|
param_in: &LoginParam,
|
||||||
) -> Result<LoginParam> {
|
) -> Result<LoginParam, Error> {
|
||||||
let xml_raw = read_url(context, url)?;
|
let xml_raw = read_url(context, url)?;
|
||||||
|
|
||||||
let res = parse_xml(¶m_in.addr, &xml_raw);
|
let res = parse_xml(¶m_in.addr, &xml_raw);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//! Outlook's Autodiscover
|
//! Outlook's Autodiscover
|
||||||
|
|
||||||
use quick_xml;
|
|
||||||
use quick_xml::events::BytesEnd;
|
use quick_xml::events::BytesEnd;
|
||||||
|
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
@@ -8,33 +7,7 @@ use crate::context::Context;
|
|||||||
use crate::login_param::LoginParam;
|
use crate::login_param::LoginParam;
|
||||||
|
|
||||||
use super::read_url::read_url;
|
use super::read_url::read_url;
|
||||||
|
use super::Error;
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum Error {
|
|
||||||
#[fail(display = "XML error at position {}", position)]
|
|
||||||
InvalidXml {
|
|
||||||
position: usize,
|
|
||||||
#[cause]
|
|
||||||
error: quick_xml::Error,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[fail(display = "Bad or incomplete autoconfig")]
|
|
||||||
IncompleteAutoconfig(LoginParam),
|
|
||||||
|
|
||||||
#[fail(display = "Failed to get URL {}", _0)]
|
|
||||||
ReadUrlError(#[cause] super::read_url::Error),
|
|
||||||
|
|
||||||
#[fail(display = "Number of redirection is exceeded")]
|
|
||||||
RedirectionError,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
impl From<super::read_url::Error> for Error {
|
|
||||||
fn from(err: super::read_url::Error) -> Error {
|
|
||||||
Error::ReadUrlError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct OutlookAutodiscover {
|
struct OutlookAutodiscover {
|
||||||
pub out: LoginParam,
|
pub out: LoginParam,
|
||||||
@@ -52,7 +25,7 @@ enum ParsingResult {
|
|||||||
RedirectUrl(String),
|
RedirectUrl(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult> {
|
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
|
||||||
let mut outlk_ad = OutlookAutodiscover {
|
let mut outlk_ad = OutlookAutodiscover {
|
||||||
out: LoginParam::new(),
|
out: LoginParam::new(),
|
||||||
out_imap_set: false,
|
out_imap_set: false,
|
||||||
@@ -143,7 +116,7 @@ pub fn outlk_autodiscover(
|
|||||||
context: &Context,
|
context: &Context,
|
||||||
url: &str,
|
url: &str,
|
||||||
_param_in: &LoginParam,
|
_param_in: &LoginParam,
|
||||||
) -> Result<LoginParam> {
|
) -> Result<LoginParam, Error> {
|
||||||
let mut url = url.to_string();
|
let mut url = url.to_string();
|
||||||
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
|
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
|
||||||
for _i in 0..10 {
|
for _i in 0..10 {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod auto_mozilla;
|
|||||||
mod auto_outlook;
|
mod auto_outlook;
|
||||||
mod read_url;
|
mod read_url;
|
||||||
|
|
||||||
|
use anyhow::{bail, ensure, Result};
|
||||||
use async_std::prelude::*;
|
use async_std::prelude::*;
|
||||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||||
|
|
||||||
@@ -11,7 +12,6 @@ use crate::config::Config;
|
|||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::error::{Error, Result};
|
|
||||||
use crate::imap::Imap;
|
use crate::imap::Imap;
|
||||||
use crate::login_param::{CertificateChecks, LoginParam};
|
use crate::login_param::{CertificateChecks, LoginParam};
|
||||||
use crate::message::Message;
|
use crate::message::Message;
|
||||||
@@ -153,7 +153,7 @@ impl Context {
|
|||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
progress!(self, 0);
|
progress!(self, 0);
|
||||||
Err(Error::Message("Configure failed".to_string()))
|
bail!("Configure failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -636,6 +636,28 @@ async fn try_smtp_one_param(context: &Context, param: &LoginParam, smtp: &mut Sm
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Invalid email address: {0:?}")]
|
||||||
|
InvalidEmailAddress(String),
|
||||||
|
|
||||||
|
#[error("XML error at position {position}")]
|
||||||
|
InvalidXml {
|
||||||
|
position: usize,
|
||||||
|
#[source]
|
||||||
|
error: quick_xml::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("Bad or incomplete autoconfig")]
|
||||||
|
IncompleteAutoconfig(LoginParam),
|
||||||
|
|
||||||
|
#[error("Failed to get URL")]
|
||||||
|
ReadUrlError(#[from] self::read_url::Error),
|
||||||
|
|
||||||
|
#[error("Number of redirection is exceeded")]
|
||||||
|
RedirectionError,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[fail(display = "URL request error")]
|
#[error("URL request error")]
|
||||||
GetError(#[cause] reqwest::Error),
|
GetError(#[from] reqwest::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub fn read_url(context: &Context, url: &str) -> Result<String, Error> {
|
||||||
|
|
||||||
pub fn read_url(context: &Context, url: &str) -> Result<String> {
|
|
||||||
info!(context, "Requesting URL {}", url);
|
info!(context, "Requesting URL {}", url);
|
||||||
|
|
||||||
match reqwest::blocking::Client::new()
|
match reqwest::blocking::Client::new()
|
||||||
|
|||||||
@@ -57,6 +57,19 @@ impl Default for ShowEmails {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum MediaQuality {
|
||||||
|
Balanced = 0,
|
||||||
|
Worse = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MediaQuality {
|
||||||
|
fn default() -> Self {
|
||||||
|
MediaQuality::Balanced // also change Config.MediaQuality props(default) on changes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum KeyGenType {
|
pub enum KeyGenType {
|
||||||
@@ -314,8 +327,6 @@ const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
|
|||||||
const DC_STR_NOMESSAGES: usize = 1;
|
const DC_STR_NOMESSAGES: usize = 1;
|
||||||
const DC_STR_SELF: usize = 2;
|
const DC_STR_SELF: usize = 2;
|
||||||
const DC_STR_DRAFT: usize = 3;
|
const DC_STR_DRAFT: usize = 3;
|
||||||
const DC_STR_MEMBER: usize = 4;
|
|
||||||
const DC_STR_CONTACT: usize = 6;
|
|
||||||
const DC_STR_VOICEMESSAGE: usize = 7;
|
const DC_STR_VOICEMESSAGE: usize = 7;
|
||||||
const DC_STR_DEADDROP: usize = 8;
|
const DC_STR_DEADDROP: usize = 8;
|
||||||
const DC_STR_IMAGE: usize = 9;
|
const DC_STR_IMAGE: usize = 9;
|
||||||
@@ -347,7 +358,6 @@ const DC_STR_ARCHIVEDCHATS: usize = 40;
|
|||||||
const DC_STR_STARREDMSGS: usize = 41;
|
const DC_STR_STARREDMSGS: usize = 41;
|
||||||
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
|
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
|
||||||
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
|
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
|
||||||
const DC_STR_SELFTALK_SUBTITLE: usize = 50;
|
|
||||||
const DC_STR_CANNOT_LOGIN: usize = 60;
|
const DC_STR_CANNOT_LOGIN: usize = 60;
|
||||||
const DC_STR_SERVER_RESPONSE: usize = 61;
|
const DC_STR_SERVER_RESPONSE: usize = 61;
|
||||||
const DC_STR_MSGACTIONBYUSER: usize = 62;
|
const DC_STR_MSGACTIONBYUSER: usize = 62;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
use async_std::path::PathBuf;
|
use async_std::path::PathBuf;
|
||||||
use deltachat_derive::*;
|
use deltachat_derive::*;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rusqlite;
|
|
||||||
|
|
||||||
use crate::aheader::EncryptPreference;
|
use crate::aheader::EncryptPreference;
|
||||||
use crate::chat::ChatId;
|
use crate::chat::ChatId;
|
||||||
@@ -11,10 +10,9 @@ use crate::config::Config;
|
|||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::e2ee;
|
use crate::error::{bail, ensure, format_err, Result};
|
||||||
use crate::error::{Error, Result};
|
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
use crate::key::*;
|
use crate::key::{DcKey, Key, SignedPublicKey};
|
||||||
use crate::login_param::LoginParam;
|
use crate::login_param::LoginParam;
|
||||||
use crate::message::{MessageState, MsgId};
|
use crate::message::{MessageState, MsgId};
|
||||||
use crate::mimeparser::AvatarAction;
|
use crate::mimeparser::AvatarAction;
|
||||||
@@ -22,9 +20,6 @@ use crate::param::*;
|
|||||||
use crate::peerstate::*;
|
use crate::peerstate::*;
|
||||||
use crate::stock::StockMessage;
|
use crate::stock::StockMessage;
|
||||||
|
|
||||||
/// Contacts with at least this origin value are shown in the contact list.
|
|
||||||
const DC_ORIGIN_MIN_CONTACT_LIST: i32 = 0x100;
|
|
||||||
|
|
||||||
/// An object representing a single contact in memory.
|
/// An object representing a single contact in memory.
|
||||||
///
|
///
|
||||||
/// The contact object is not updated.
|
/// The contact object is not updated.
|
||||||
@@ -92,6 +87,7 @@ pub enum Origin {
|
|||||||
UnhandledQrScan = 0x80,
|
UnhandledQrScan = 0x80,
|
||||||
|
|
||||||
/// Reply-To: of incoming message of known sender
|
/// Reply-To: of incoming message of known sender
|
||||||
|
/// Contacts with at least this origin value are shown in the contact list.
|
||||||
IncomingReplyTo = 0x100,
|
IncomingReplyTo = 0x100,
|
||||||
|
|
||||||
/// Cc: of incoming message of known sender
|
/// Cc: of incoming message of known sender
|
||||||
@@ -285,7 +281,11 @@ impl Contact {
|
|||||||
///
|
///
|
||||||
/// To validate an e-mail address independently of the contact database
|
/// To validate an e-mail address independently of the contact database
|
||||||
/// use `dc_may_be_valid_addr()`.
|
/// use `dc_may_be_valid_addr()`.
|
||||||
pub async fn lookup_id_by_addr(context: &Context, addr: impl AsRef<str>) -> u32 {
|
pub async fn lookup_id_by_addr(
|
||||||
|
context: &Context,
|
||||||
|
addr: impl AsRef<str>,
|
||||||
|
min_origin: Origin,
|
||||||
|
) -> u32 {
|
||||||
if addr.as_ref().is_empty() {
|
if addr.as_ref().is_empty() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -299,14 +299,13 @@ impl Contact {
|
|||||||
if addr_cmp(addr_normalized, addr_self) {
|
if addr_cmp(addr_normalized, addr_self) {
|
||||||
return DC_CONTACT_ID_SELF;
|
return DC_CONTACT_ID_SELF;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.sql.query_get_value(
|
context.sql.query_get_value(
|
||||||
context,
|
context,
|
||||||
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
|
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||||
paramsv![
|
paramsv![
|
||||||
addr_normalized,
|
addr_normalized,
|
||||||
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
DC_CONTACT_ID_LAST_SPECIAL as i32,
|
||||||
DC_ORIGIN_MIN_CONTACT_LIST,
|
min_origin as u32,
|
||||||
],
|
],
|
||||||
).await.unwrap_or_default()
|
).await.unwrap_or_default()
|
||||||
}
|
}
|
||||||
@@ -677,8 +676,6 @@ impl Contact {
|
|||||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await;
|
let peerstate = Peerstate::from_addr(context, &contact.addr).await;
|
||||||
let loginparam = LoginParam::from_database(context, "configured_").await;
|
let loginparam = LoginParam::from_database(context, "configured_").await;
|
||||||
|
|
||||||
let mut self_key = Key::from_self_public(context, &loginparam.addr, &context.sql).await;
|
|
||||||
|
|
||||||
if peerstate.is_some()
|
if peerstate.is_some()
|
||||||
&& peerstate
|
&& peerstate
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -694,16 +691,11 @@ impl Contact {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
ret += &p;
|
ret += &p;
|
||||||
if self_key.is_none() {
|
let self_key = Key::from(SignedPublicKey::load_self(context).await?);
|
||||||
e2ee::ensure_secret_key_exists(context).await?;
|
|
||||||
self_key = Key::from_self_public(context, &loginparam.addr, &context.sql).await;
|
|
||||||
}
|
|
||||||
let p = context.stock_str(StockMessage::FingerPrints).await;
|
let p = context.stock_str(StockMessage::FingerPrints).await;
|
||||||
ret += &format!(" {}:", p);
|
ret += &format!(" {}:", p);
|
||||||
|
|
||||||
let fingerprint_self = self_key
|
let fingerprint_self = self_key.formatted_fingerprint();
|
||||||
.map(|k| k.formatted_fingerprint())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let fingerprint_other_verified = peerstate
|
let fingerprint_other_verified = peerstate
|
||||||
.peek_key(PeerstateVerifiedStatus::BidirectVerified)
|
.peek_key(PeerstateVerifiedStatus::BidirectVerified)
|
||||||
.map(|k| k.formatted_fingerprint())
|
.map(|k| k.formatted_fingerprint())
|
||||||
@@ -1160,10 +1152,10 @@ fn cat_fingerprint(
|
|||||||
impl Context {
|
impl Context {
|
||||||
/// determine whether the specified addr maps to the/a self addr
|
/// determine whether the specified addr maps to the/a self addr
|
||||||
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
|
||||||
let self_addr = match self.get_config(Config::ConfiguredAddr).await {
|
let self_addr = self
|
||||||
Some(s) => s,
|
.get_config(Config::ConfiguredAddr)
|
||||||
None => return Err(Error::NotConfigured),
|
.await
|
||||||
};
|
.ok_or_else(|| format_err!("Not configured"))?;
|
||||||
|
|
||||||
Ok(addr_cmp(self_addr, addr))
|
Ok(addr_cmp(self_addr, addr))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
//! Context module
|
//! Context module
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use async_std::path::{Path, PathBuf};
|
use async_std::path::{Path, PathBuf};
|
||||||
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
|
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
|
||||||
use crossbeam_queue::SegQueue;
|
use crossbeam_queue::SegQueue;
|
||||||
@@ -12,16 +13,18 @@ use crate::chat::*;
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
|
use crate::dc_tools::duration_to_str;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
use crate::job::{self, Action};
|
use crate::job::{self, Action};
|
||||||
use crate::key::Key;
|
use crate::key::{DcKey, Key, SignedPublicKey};
|
||||||
use crate::login_param::LoginParam;
|
use crate::login_param::LoginParam;
|
||||||
use crate::lot::Lot;
|
use crate::lot::Lot;
|
||||||
use crate::message::{self, Message, MessengerMessage, MsgId};
|
use crate::message::{self, Message, MessengerMessage, MsgId};
|
||||||
use crate::param::Params;
|
use crate::param::Params;
|
||||||
use crate::scheduler::Scheduler;
|
use crate::scheduler::Scheduler;
|
||||||
use crate::sql::Sql;
|
use crate::sql::Sql;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
@@ -53,6 +56,8 @@ pub struct InnerContext {
|
|||||||
pub(crate) logs: SegQueue<Event>,
|
pub(crate) logs: SegQueue<Event>,
|
||||||
|
|
||||||
pub(crate) scheduler: RwLock<Scheduler>,
|
pub(crate) scheduler: RwLock<Scheduler>,
|
||||||
|
|
||||||
|
creation_time: SystemTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -68,8 +73,8 @@ pub struct RunningState {
|
|||||||
/// actual keys and their values which will be present are not
|
/// actual keys and their values which will be present are not
|
||||||
/// guaranteed. Calling [Context::get_info] also includes information
|
/// guaranteed. Calling [Context::get_info] also includes information
|
||||||
/// about the context on top of the information here.
|
/// about the context on top of the information here.
|
||||||
pub fn get_info() -> HashMap<&'static str, String> {
|
pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||||
let mut res = HashMap::new();
|
let mut res = BTreeMap::new();
|
||||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||||
res.insert("sqlite_version", rusqlite::version().to_string());
|
res.insert("sqlite_version", rusqlite::version().to_string());
|
||||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||||
@@ -115,6 +120,7 @@ impl Context {
|
|||||||
translated_stockstrings: RwLock::new(HashMap::new()),
|
translated_stockstrings: RwLock::new(HashMap::new()),
|
||||||
logs: SegQueue::new(),
|
logs: SegQueue::new(),
|
||||||
scheduler: RwLock::new(Scheduler::Stopped),
|
scheduler: RwLock::new(Scheduler::Stopped),
|
||||||
|
creation_time: std::time::SystemTime::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
@@ -168,7 +174,7 @@ impl Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_next_event(&self) -> Result<Event> {
|
pub fn get_next_event(&self) -> Result<Event> {
|
||||||
let event = self.logs.pop()?;
|
let event = self.logs.pop().map_err(|err| anyhow!("{}", err))?;
|
||||||
|
|
||||||
Ok(event)
|
Ok(event)
|
||||||
}
|
}
|
||||||
@@ -237,7 +243,7 @@ impl Context {
|
|||||||
* UI chat/message related API
|
* UI chat/message related API
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
pub async fn get_info(&self) -> HashMap<&'static str, String> {
|
pub async fn get_info(&self) -> BTreeMap<&'static str, String> {
|
||||||
let unset = "0";
|
let unset = "0";
|
||||||
let l = LoginParam::from_database(self, "").await;
|
let l = LoginParam::from_database(self, "").await;
|
||||||
let l2 = LoginParam::from_database(self, "configured_").await;
|
let l2 = LoginParam::from_database(self, "configured_").await;
|
||||||
@@ -266,13 +272,10 @@ impl Context {
|
|||||||
.sql
|
.sql
|
||||||
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
|
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
|
||||||
.await;
|
.await;
|
||||||
|
let fingerprint_str = match SignedPublicKey::load_self(self).await {
|
||||||
let fingerprint_str =
|
Ok(key) => Key::from(key).fingerprint(),
|
||||||
if let Some(key) = Key::from_self_public(self, &l2.addr, &self.sql).await {
|
Err(err) => format!("<key failure: {}>", err),
|
||||||
key.fingerprint()
|
};
|
||||||
} else {
|
|
||||||
"<Not yet calculated>".into()
|
|
||||||
};
|
|
||||||
|
|
||||||
let inbox_watch = self.get_config_int(Config::InboxWatch).await;
|
let inbox_watch = self.get_config_int(Config::InboxWatch).await;
|
||||||
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await;
|
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await;
|
||||||
@@ -333,6 +336,9 @@ impl Context {
|
|||||||
);
|
);
|
||||||
res.insert("fingerprint", fingerprint_str);
|
res.insert("fingerprint", fingerprint_str);
|
||||||
|
|
||||||
|
let elapsed = self.creation_time.elapsed();
|
||||||
|
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||||
|
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ use itertools::join;
|
|||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use mailparse::SingleInfo;
|
||||||
|
|
||||||
use crate::chat::{self, Chat, ChatId};
|
use crate::chat::{self, Chat, ChatId};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::error::Result;
|
use crate::error::{bail, ensure, Result};
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
use crate::headerdef::HeaderDef;
|
use crate::headerdef::HeaderDef;
|
||||||
use crate::job::{self, Action};
|
use crate::job::{self, Action};
|
||||||
@@ -30,6 +32,10 @@ enum CreateEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Receive a message and add it to the database.
|
/// Receive a message and add it to the database.
|
||||||
|
///
|
||||||
|
/// Returns an error on recoverable errors, e.g. database errors. In this case,
|
||||||
|
/// message parsing should be retried later. If message itself is wrong, logs
|
||||||
|
/// the error and returns success.
|
||||||
pub async fn dc_receive_imf(
|
pub async fn dc_receive_imf(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
imf_raw: &[u8],
|
imf_raw: &[u8],
|
||||||
@@ -53,10 +59,19 @@ pub async fn dc_receive_imf(
|
|||||||
println!("{}", String::from_utf8_lossy(imf_raw));
|
println!("{}", String::from_utf8_lossy(imf_raw));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut mime_parser = MimeMessage::from_bytes(context, imf_raw).await?;
|
let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw).await {
|
||||||
|
Err(err) => {
|
||||||
|
warn!(context, "dc_receive_imf: can't parse MIME: {}", err);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Ok(mime_parser) => mime_parser,
|
||||||
|
};
|
||||||
|
|
||||||
// we can not add even an empty record if we have no info whatsoever
|
// we can not add even an empty record if we have no info whatsoever
|
||||||
ensure!(mime_parser.has_headers(), "No Headers Found");
|
if !mime_parser.has_headers() {
|
||||||
|
warn!(context, "dc_receive_imf: no headers found");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// the function returns the number of created messages in the database
|
// the function returns the number of created messages in the database
|
||||||
let mut chat_id = ChatId::new(0);
|
let mut chat_id = ChatId::new(0);
|
||||||
@@ -95,32 +110,26 @@ pub async fn dc_receive_imf(
|
|||||||
// we do not check Return-Path any more as this is unreliable, see
|
// we do not check Return-Path any more as this is unreliable, see
|
||||||
// https://github.com/deltachat/deltachat-core/issues/150)
|
// https://github.com/deltachat/deltachat-core/issues/150)
|
||||||
let (from_id, from_id_blocked, incoming_origin) =
|
let (from_id, from_id_blocked, incoming_origin) =
|
||||||
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
|
from_field_to_contact_id(context, &mime_parser.from).await?;
|
||||||
from_field_to_contact_id(context, field_from).await?
|
|
||||||
} else {
|
|
||||||
(0, false, Origin::Unknown)
|
|
||||||
};
|
|
||||||
let incoming = from_id != DC_CONTACT_ID_SELF;
|
let incoming = from_id != DC_CONTACT_ID_SELF;
|
||||||
|
|
||||||
let mut to_ids = ContactIds::new();
|
let mut to_ids = ContactIds::new();
|
||||||
for header_def in &[HeaderDef::To, HeaderDef::Cc] {
|
|
||||||
if let Some(field) = mime_parser.get(header_def.clone()) {
|
to_ids.extend(
|
||||||
to_ids.extend(
|
&dc_add_or_lookup_contacts_by_address_list(
|
||||||
&dc_add_or_lookup_contacts_by_address_list(
|
context,
|
||||||
context,
|
&mime_parser.recipients,
|
||||||
&field,
|
if !incoming {
|
||||||
if !incoming {
|
Origin::OutgoingTo
|
||||||
Origin::OutgoingTo
|
} else if incoming_origin.is_known() {
|
||||||
} else if incoming_origin.is_known() {
|
Origin::IncomingTo
|
||||||
Origin::IncomingTo
|
} else {
|
||||||
} else {
|
Origin::IncomingUnknownTo
|
||||||
Origin::IncomingUnknownTo
|
},
|
||||||
},
|
)
|
||||||
)
|
.await?,
|
||||||
.await?,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add parts
|
// Add parts
|
||||||
|
|
||||||
@@ -238,11 +247,11 @@ pub async fn dc_receive_imf(
|
|||||||
/// Also returns whether it is blocked or not and its origin.
|
/// Also returns whether it is blocked or not and its origin.
|
||||||
pub async fn from_field_to_contact_id(
|
pub async fn from_field_to_contact_id(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
field_from: &str,
|
from_address_list: &[SingleInfo],
|
||||||
) -> Result<(u32, bool, Origin)> {
|
) -> Result<(u32, bool, Origin)> {
|
||||||
let from_ids = dc_add_or_lookup_contacts_by_address_list(
|
let from_ids = dc_add_or_lookup_contacts_by_address_list(
|
||||||
context,
|
context,
|
||||||
&field_from,
|
from_address_list,
|
||||||
Origin::IncomingUnknownFrom,
|
Origin::IncomingUnknownFrom,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -253,7 +262,7 @@ pub async fn from_field_to_contact_id(
|
|||||||
if from_ids.len() > 1 {
|
if from_ids.len() > 1 {
|
||||||
warn!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
"mail has more than one From address, only using first: {:?}", field_from
|
"mail has more than one From address, only using first: {:?}", from_address_list
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let from_id = from_ids.get_index(0).cloned().unwrap_or_default();
|
let from_id = from_ids.get_index(0).cloned().unwrap_or_default();
|
||||||
@@ -266,7 +275,10 @@ pub async fn from_field_to_contact_id(
|
|||||||
}
|
}
|
||||||
Ok((from_id, from_id_blocked, incoming_origin))
|
Ok((from_id, from_id_blocked, incoming_origin))
|
||||||
} else {
|
} else {
|
||||||
warn!(context, "mail has an empty From header: {:?}", field_from);
|
warn!(
|
||||||
|
context,
|
||||||
|
"mail has an empty From header: {:?}", from_address_list
|
||||||
|
);
|
||||||
// if there is no from given, from_id stays 0 which is just fine. These messages
|
// if there is no from given, from_id stays 0 which is just fine. These messages
|
||||||
// are very rare, however, we have to add them to the database (they go to the
|
// are very rare, however, we have to add them to the database (they go to the
|
||||||
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
|
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
|
||||||
@@ -308,15 +320,16 @@ async fn add_parts(
|
|||||||
// check, if the mail is already in our database - if so, just update the folder/uid
|
// check, if the mail is already in our database - if so, just update the folder/uid
|
||||||
// (if the mail was moved around) and finish. (we may get a mail twice eg. if it is
|
// (if the mail was moved around) and finish. (we may get a mail twice eg. if it is
|
||||||
// moved between folders. make sure, this check is done eg. before securejoin-processing) */
|
// moved between folders. make sure, this check is done eg. before securejoin-processing) */
|
||||||
if let Ok((old_server_folder, old_server_uid, _)) =
|
if let Some((old_server_folder, old_server_uid, _)) =
|
||||||
message::rfc724_mid_exists(context, &rfc724_mid).await
|
message::rfc724_mid_exists(context, &rfc724_mid).await?
|
||||||
{
|
{
|
||||||
if old_server_folder != server_folder.as_ref() || old_server_uid != server_uid {
|
if old_server_folder != server_folder.as_ref() || old_server_uid != server_uid {
|
||||||
message::update_server_uid(context, &rfc724_mid, server_folder.as_ref(), server_uid)
|
message::update_server_uid(context, &rfc724_mid, server_folder.as_ref(), server_uid)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
bail!("Message already in DB");
|
warn!(context, "Message already in DB");
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut msgrmsg = if mime_parser.has_chat_version() {
|
let mut msgrmsg = if mime_parser.has_chat_version() {
|
||||||
@@ -378,9 +391,11 @@ async fn add_parts(
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
*hidden = true;
|
*hidden = true;
|
||||||
|
|
||||||
context.bob.write().await.status = 0; // secure-join failed
|
context.bob.write().await.status = 0; // secure-join failed
|
||||||
context.stop_ongoing().await;
|
context.stop_ongoing().await;
|
||||||
error!(context, "Error in Secure-Join message handling: {}", err);
|
warn!(context, "Error in Secure-Join message handling: {}", err);
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,7 +516,7 @@ async fn add_parts(
|
|||||||
msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting
|
msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting
|
||||||
*chat_id = ChatId::new(0);
|
*chat_id = ChatId::new(0);
|
||||||
allow_creation = true;
|
allow_creation = true;
|
||||||
match observe_securejoin_on_other_device(context, mime_parser, to_id) {
|
match observe_securejoin_on_other_device(context, mime_parser, to_id).await {
|
||||||
Ok(securejoin::HandshakeMessage::Done)
|
Ok(securejoin::HandshakeMessage::Done)
|
||||||
| Ok(securejoin::HandshakeMessage::Ignore) => {
|
| Ok(securejoin::HandshakeMessage::Ignore) => {
|
||||||
*hidden = true;
|
*hidden = true;
|
||||||
@@ -511,7 +526,8 @@ async fn add_parts(
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
*hidden = true;
|
*hidden = true;
|
||||||
error!(context, "Error in Secure-Join watching: {}", err);
|
warn!(context, "Error in Secure-Join watching: {}", err);
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -719,7 +735,7 @@ async fn add_parts(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// check event to send
|
// check event to send
|
||||||
if chat_id.is_trash() {
|
if chat_id.is_trash() || *hidden {
|
||||||
*create_event_to_send = None;
|
*create_event_to_send = None;
|
||||||
} else if incoming && state == MessageState::InFresh {
|
} else if incoming && state == MessageState::InFresh {
|
||||||
if from_id_blocked {
|
if from_id_blocked {
|
||||||
@@ -854,7 +870,6 @@ async fn create_or_lookup_group(
|
|||||||
let mut chat_id_blocked = Blocked::Not;
|
let mut chat_id_blocked = Blocked::Not;
|
||||||
let mut recreate_member_list = false;
|
let mut recreate_member_list = false;
|
||||||
let mut send_EVENT_CHAT_MODIFIED = false;
|
let mut send_EVENT_CHAT_MODIFIED = false;
|
||||||
let mut X_MrRemoveFromGrp = None;
|
|
||||||
let mut X_MrAddToGrp = None;
|
let mut X_MrAddToGrp = None;
|
||||||
let mut X_MrGrpNameChanged = false;
|
let mut X_MrGrpNameChanged = false;
|
||||||
let mut better_msg: String = From::from("");
|
let mut better_msg: String = From::from("");
|
||||||
@@ -904,25 +919,27 @@ async fn create_or_lookup_group(
|
|||||||
// but we might not know about this group
|
// but we might not know about this group
|
||||||
|
|
||||||
let grpname = mime_parser.get(HeaderDef::ChatGroupName).cloned();
|
let grpname = mime_parser.get(HeaderDef::ChatGroupName).cloned();
|
||||||
|
let mut removed_id = 0;
|
||||||
|
|
||||||
if let Some(optional_field) = mime_parser.get(HeaderDef::ChatGroupMemberRemoved).cloned() {
|
if let Some(removed_addr) = mime_parser.get(HeaderDef::ChatGroupMemberRemoved).cloned() {
|
||||||
X_MrRemoveFromGrp = Some(optional_field);
|
removed_id = Contact::lookup_id_by_addr(context, &removed_addr, Origin::Unknown).await;
|
||||||
mime_parser.is_system_message = SystemMessage::MemberRemovedFromGroup;
|
if removed_id == 0 {
|
||||||
let left_group = Contact::lookup_id_by_addr(context, X_MrRemoveFromGrp.as_ref().unwrap())
|
warn!(context, "removed {:?} has no contact_id", removed_addr);
|
||||||
.await
|
} else {
|
||||||
== from_id as u32;
|
mime_parser.is_system_message = SystemMessage::MemberRemovedFromGroup;
|
||||||
better_msg = context
|
better_msg = context
|
||||||
.stock_system_msg(
|
.stock_system_msg(
|
||||||
if left_group {
|
if removed_id == from_id as u32 {
|
||||||
StockMessage::MsgGroupLeft
|
StockMessage::MsgGroupLeft
|
||||||
} else {
|
} else {
|
||||||
StockMessage::MsgDelMember
|
StockMessage::MsgDelMember
|
||||||
},
|
},
|
||||||
X_MrRemoveFromGrp.as_ref().unwrap(),
|
&removed_addr,
|
||||||
"",
|
"",
|
||||||
from_id as u32,
|
from_id as u32,
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let field = mime_parser.get(HeaderDef::ChatGroupMemberAdded).cloned();
|
let field = mime_parser.get(HeaderDef::ChatGroupMemberAdded).cloned();
|
||||||
if let Some(optional_field) = field {
|
if let Some(optional_field) = field {
|
||||||
@@ -1018,7 +1035,7 @@ async fn create_or_lookup_group(
|
|||||||
&& !grpid.is_empty()
|
&& !grpid.is_empty()
|
||||||
&& grpname.is_some()
|
&& grpname.is_some()
|
||||||
// otherwise, a pending "quit" message may pop up
|
// otherwise, a pending "quit" message may pop up
|
||||||
&& X_MrRemoveFromGrp.is_none()
|
&& removed_id == 0
|
||||||
// re-create explicitly left groups only if ourself is re-added
|
// re-create explicitly left groups only if ourself is re-added
|
||||||
&& (!group_explicitly_left
|
&& (!group_explicitly_left
|
||||||
|| X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
|
|| X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
|
||||||
@@ -1156,12 +1173,8 @@ async fn create_or_lookup_group(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
send_EVENT_CHAT_MODIFIED = true;
|
send_EVENT_CHAT_MODIFIED = true;
|
||||||
} else if let Some(removed_addr) = X_MrRemoveFromGrp {
|
} else if removed_id > 0 {
|
||||||
let contact_id = Contact::lookup_id_by_addr(context, removed_addr).await;
|
chat::remove_from_chat_contacts_table(context, chat_id, removed_id).await;
|
||||||
if contact_id != 0 {
|
|
||||||
info!(context, "remove {:?} from chat id={}", contact_id, chat_id);
|
|
||||||
chat::remove_from_chat_contacts_table(context, chat_id, contact_id).await;
|
|
||||||
}
|
|
||||||
send_EVENT_CHAT_MODIFIED = true;
|
send_EVENT_CHAT_MODIFIED = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1270,10 +1283,9 @@ async fn create_or_lookup_adhoc_group(
|
|||||||
return Ok((ChatId::new(0), Blocked::Not));
|
return Ok((ChatId::new(0), Blocked::Not));
|
||||||
}
|
}
|
||||||
// use subject as initial chat name
|
// use subject as initial chat name
|
||||||
let default_name = context
|
let grpname = mime_parser
|
||||||
.stock_string_repl_int(StockMessage::Member, member_ids.len() as i32)
|
.get_subject()
|
||||||
.await;
|
.unwrap_or_else(|| "Unnamed group".to_string());
|
||||||
let grpname = mime_parser.get_subject().unwrap_or_else(|| default_name);
|
|
||||||
|
|
||||||
// create group record
|
// create group record
|
||||||
let new_chat_id: ChatId = create_group_record(
|
let new_chat_id: ChatId = create_group_record(
|
||||||
@@ -1593,7 +1605,7 @@ async fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(ids) = mailparse::msgidparse(mid_list) {
|
if let Ok(ids) = parse_message_ids(mid_list) {
|
||||||
for id in ids.iter() {
|
for id in ids.iter() {
|
||||||
if is_known_rfc724_mid(context, id).await {
|
if is_known_rfc724_mid(context, id).await {
|
||||||
return true;
|
return true;
|
||||||
@@ -1643,7 +1655,7 @@ async fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMess
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
pub(crate) async fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
||||||
if let Ok(ids) = mailparse::msgidparse(mid_list) {
|
if let Ok(ids) = parse_message_ids(mid_list) {
|
||||||
for id in ids.iter() {
|
for id in ids.iter() {
|
||||||
if is_msgrmsg_rfc724_mid(context, id).await {
|
if is_msgrmsg_rfc724_mid(context, id).await {
|
||||||
return true;
|
return true;
|
||||||
@@ -1669,39 +1681,14 @@ async fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
|||||||
|
|
||||||
async fn dc_add_or_lookup_contacts_by_address_list(
|
async fn dc_add_or_lookup_contacts_by_address_list(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
addr_list_raw: &str,
|
address_list: &[SingleInfo],
|
||||||
origin: Origin,
|
origin: Origin,
|
||||||
) -> Result<ContactIds> {
|
) -> Result<ContactIds> {
|
||||||
let addrs = match mailparse::addrparse(addr_list_raw) {
|
|
||||||
Ok(addrs) => addrs,
|
|
||||||
Err(err) => {
|
|
||||||
bail!("could not parse {:?}: {:?}", addr_list_raw, err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut contact_ids = ContactIds::new();
|
let mut contact_ids = ContactIds::new();
|
||||||
for addr in addrs.iter() {
|
for info in address_list.iter() {
|
||||||
match addr {
|
contact_ids.insert(
|
||||||
mailparse::MailAddr::Single(info) => {
|
add_or_lookup_contact_by_addr(context, &info.display_name, &info.addr, origin).await?,
|
||||||
contact_ids.insert(
|
);
|
||||||
add_or_lookup_contact_by_addr(context, &info.display_name, &info.addr, origin)
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
mailparse::MailAddr::Group(infos) => {
|
|
||||||
for info in &infos.addrs {
|
|
||||||
contact_ids.insert(
|
|
||||||
add_or_lookup_contact_by_addr(
|
|
||||||
context,
|
|
||||||
&info.display_name,
|
|
||||||
&info.addr,
|
|
||||||
origin,
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(contact_ids)
|
Ok(contact_ids)
|
||||||
@@ -1748,6 +1735,7 @@ fn dc_create_incoming_rfc724_mid(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::chat::ChatVisibility;
|
||||||
use crate::chatlist::Chatlist;
|
use crate::chatlist::Chatlist;
|
||||||
use crate::message::Message;
|
use crate::message::Message;
|
||||||
use crate::test_utils::{dummy_context, TestContext};
|
use crate::test_utils::{dummy_context, TestContext};
|
||||||
@@ -2020,4 +2008,308 @@ mod tests {
|
|||||||
assert_eq!(chat.name, "group with Alice, Bob and Claire");
|
assert_eq!(chat.name, "group with Alice, Bob and Claire");
|
||||||
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 3);
|
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await.len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_read_receipt_and_unarchive() {
|
||||||
|
// create alice's account
|
||||||
|
let t = configured_offline_context().await;
|
||||||
|
|
||||||
|
// create one-to-one with bob, archive one-to-one
|
||||||
|
let bob_id = Contact::create(&t.ctx, "bob", "bob@exampel.org")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let one2one_id = chat::create_by_contact_id(&t.ctx, bob_id).await.unwrap();
|
||||||
|
one2one_id
|
||||||
|
.set_visibility(&t.ctx, ChatVisibility::Archived)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let one2one = Chat::load_from_db(&t.ctx, one2one_id).await.unwrap();
|
||||||
|
assert!(one2one.get_visibility() == ChatVisibility::Archived);
|
||||||
|
|
||||||
|
// create a group with bob, archive group
|
||||||
|
let group_id = chat::create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
chat::add_contact_to_chat(&t.ctx, group_id, bob_id).await;
|
||||||
|
assert_eq!(
|
||||||
|
chat::get_chat_msgs(&t.ctx, group_id, 0, None).await.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
group_id
|
||||||
|
.set_visibility(&t.ctx, ChatVisibility::Archived)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let group = Chat::load_from_db(&t.ctx, group_id).await.unwrap();
|
||||||
|
assert!(group.get_visibility() == ChatVisibility::Archived);
|
||||||
|
|
||||||
|
// everything archived, chatlist should be empty
|
||||||
|
assert_eq!(
|
||||||
|
Chatlist::try_load(&t.ctx, DC_GCL_NO_SPECIALS, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// send a message to group with bob
|
||||||
|
dc_receive_imf(
|
||||||
|
&t.ctx,
|
||||||
|
format!(
|
||||||
|
"From: alice@example.org\n\
|
||||||
|
To: bob@example.org\n\
|
||||||
|
Subject: foo\n\
|
||||||
|
Message-ID: <Gr.{}.12345678901@example.org>\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Chat-Group-ID: {}\n\
|
||||||
|
Chat-Group-Name: foo\n\
|
||||||
|
Chat-Disposition-Notification-To: alice@example.org\n\
|
||||||
|
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||||
|
\n\
|
||||||
|
hello\n",
|
||||||
|
group.grpid, group.grpid
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
"INBOX",
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let msgs = chat::get_chat_msgs(&t.ctx, group_id, 0, None).await;
|
||||||
|
assert_eq!(msgs.len(), 1);
|
||||||
|
let msg_id = msgs.first().unwrap();
|
||||||
|
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||||
|
assert_eq!(msg.text.unwrap(), "hello");
|
||||||
|
assert_eq!(msg.state, MessageState::OutDelivered);
|
||||||
|
let group = Chat::load_from_db(&t.ctx, group_id).await.unwrap();
|
||||||
|
assert!(group.get_visibility() == ChatVisibility::Normal);
|
||||||
|
|
||||||
|
// bob sends a read receipt to the group
|
||||||
|
dc_receive_imf(
|
||||||
|
&t.ctx,
|
||||||
|
format!(
|
||||||
|
"From: bob@example.org\n\
|
||||||
|
To: alice@example.org\n\
|
||||||
|
Subject: message opened\n\
|
||||||
|
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Message-ID: <Mr.12345678902@example.org>\n\
|
||||||
|
Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\
|
||||||
|
\n\
|
||||||
|
\n\
|
||||||
|
--SNIPP\n\
|
||||||
|
Content-Type: text/plain; charset=utf-8\n\
|
||||||
|
\n\
|
||||||
|
Read receipts do not guarantee sth. was read.\n\
|
||||||
|
\n\
|
||||||
|
\n\
|
||||||
|
--SNIPP\n\
|
||||||
|
Content-Type: message/disposition-notification\n\
|
||||||
|
\n\
|
||||||
|
Reporting-UA: Delta Chat 1.28.0\n\
|
||||||
|
Original-Recipient: rfc822;bob@example.org\n\
|
||||||
|
Final-Recipient: rfc822;bob@example.org\n\
|
||||||
|
Original-Message-ID: <Gr.{}.12345678901@example.org>\n\
|
||||||
|
Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||||
|
\n\
|
||||||
|
\n\
|
||||||
|
--SNIPP--",
|
||||||
|
group.grpid
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
"INBOX",
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
chat::get_chat_msgs(&t.ctx, group_id, 0, None).await.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(msg.state, MessageState::OutMdnRcvd);
|
||||||
|
|
||||||
|
// check, the read-receipt has not unarchived the one2one
|
||||||
|
assert_eq!(
|
||||||
|
Chatlist::try_load(&t.ctx, DC_GCL_NO_SPECIALS, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
let one2one = Chat::load_from_db(&t.ctx, one2one_id).await.unwrap();
|
||||||
|
assert!(one2one.get_visibility() == ChatVisibility::Archived);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_no_from() {
|
||||||
|
// if there is no from given, from_id stays 0 which is just fine. These messages
|
||||||
|
// are very rare, however, we have to add them to the database (they go to the
|
||||||
|
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
|
||||||
|
|
||||||
|
let t = configured_offline_context().await;
|
||||||
|
let context = &t.ctx;
|
||||||
|
|
||||||
|
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||||
|
assert!(chats.get_msg_id(0).is_err());
|
||||||
|
|
||||||
|
dc_receive_imf(
|
||||||
|
context,
|
||||||
|
b"To: bob@example.org\n\
|
||||||
|
Subject: foo\n\
|
||||||
|
Message-ID: <3924@example.org>\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||||
|
\n\
|
||||||
|
hello\n",
|
||||||
|
"INBOX",
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||||
|
// Check that the message was added to the database:
|
||||||
|
assert!(chats.get_msg_id(0).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_escaped_from() {
|
||||||
|
let t = configured_offline_context().await;
|
||||||
|
let contact_id = Contact::create(&t.ctx, "foobar", "foobar@example.com")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let chat_id = chat::create_by_contact_id(&t.ctx, contact_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
dc_receive_imf(
|
||||||
|
&t.ctx,
|
||||||
|
b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>\n\
|
||||||
|
To: alice@example.org\n\
|
||||||
|
Subject: foo\n\
|
||||||
|
Message-ID: <asdklfjjaweofi@example.org>\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Chat-Disposition-Notification-To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>\n\
|
||||||
|
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||||
|
\n\
|
||||||
|
hello\n",
|
||||||
|
"INBOX",
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
Contact::load_from_db(&t.ctx, contact_id)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.get_authname(),
|
||||||
|
"Фамилия Имя", // The name was "Имя, Фамилия" and ("lastname, firstname") and should be swapped to "firstname, lastname"
|
||||||
|
);
|
||||||
|
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await;
|
||||||
|
assert_eq!(msgs.len(), 1);
|
||||||
|
let msg_id = msgs.first().unwrap();
|
||||||
|
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||||
|
assert_eq!(msg.text.unwrap(), "hello");
|
||||||
|
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_escaped_recipients() {
|
||||||
|
let t = configured_offline_context().await;
|
||||||
|
Contact::create(&t.ctx, "foobar", "foobar@example.com")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let carl_contact_id =
|
||||||
|
Contact::add_or_lookup(&t.ctx, "Carl", "carl@host.tld", Origin::IncomingUnknownFrom)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
|
||||||
|
dc_receive_imf(
|
||||||
|
&t.ctx,
|
||||||
|
b"From: Foobar <foobar@example.com>\n\
|
||||||
|
To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= alice@example.org\n\
|
||||||
|
Cc: =?utf-8?q?=3Ch2=3E?= <carl@host.tld>\n\
|
||||||
|
Subject: foo\n\
|
||||||
|
Message-ID: <asdklfjjaweofi@example.org>\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Chat-Disposition-Notification-To: <foobar@example.com>\n\
|
||||||
|
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||||
|
\n\
|
||||||
|
hello\n",
|
||||||
|
"INBOX",
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
Contact::load_from_db(&t.ctx, carl_contact_id)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.get_name(),
|
||||||
|
"h2"
|
||||||
|
);
|
||||||
|
|
||||||
|
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||||
|
let msg = Message::load_from_db(&t.ctx, chats.get_msg_id(0).unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
|
||||||
|
assert_eq!(msg.text.unwrap(), "hello");
|
||||||
|
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_cc_to_contact() {
|
||||||
|
let t = configured_offline_context().await;
|
||||||
|
Contact::create(&t.ctx, "foobar", "foobar@example.com")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let carl_contact_id = Contact::add_or_lookup(
|
||||||
|
&t.ctx,
|
||||||
|
"garabage",
|
||||||
|
"carl@host.tld",
|
||||||
|
Origin::IncomingUnknownFrom,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
|
||||||
|
dc_receive_imf(
|
||||||
|
&t.ctx,
|
||||||
|
b"From: Foobar <foobar@example.com>\n\
|
||||||
|
To: alice@example.org\n\
|
||||||
|
Cc: Carl <carl@host.tld>\n\
|
||||||
|
Subject: foo\n\
|
||||||
|
Message-ID: <asdklfjjaweofi@example.org>\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Chat-Disposition-Notification-To: <foobar@example.com>\n\
|
||||||
|
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||||
|
\n\
|
||||||
|
hello\n",
|
||||||
|
"INBOX",
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
Contact::load_from_db(&t.ctx, carl_contact_id)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.get_name(),
|
||||||
|
"Carl"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
128
src/dc_tools.rs
128
src/dc_tools.rs
@@ -5,7 +5,7 @@ use core::cmp::{max, min};
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::SystemTime;
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use async_std::path::{Path, PathBuf};
|
use async_std::path::{Path, PathBuf};
|
||||||
use async_std::{fs, io};
|
use async_std::{fs, io};
|
||||||
@@ -13,7 +13,7 @@ use chrono::{Local, TimeZone};
|
|||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::error::Error;
|
use crate::error::{bail, Error};
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
|
|
||||||
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
|
||||||
@@ -76,6 +76,14 @@ pub fn dc_timestamp_to_str(wanted: i64) -> String {
|
|||||||
ts.format("%Y.%m.%d %H:%M:%S").to_string()
|
ts.format("%Y.%m.%d %H:%M:%S").to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn duration_to_str(duration: Duration) -> String {
|
||||||
|
let secs = duration.as_secs();
|
||||||
|
let h = secs / 3600;
|
||||||
|
let m = (secs % 3600) / 60;
|
||||||
|
let s = (secs % 3600) % 60;
|
||||||
|
format!("{}h {}m {}s", h, m, s)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn dc_gm2local_offset() -> i64 {
|
pub(crate) fn dc_gm2local_offset() -> i64 {
|
||||||
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
|
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
|
||||||
the function may return negative values. */
|
the function may return negative values. */
|
||||||
@@ -466,6 +474,23 @@ pub(crate) fn time() -> i64 {
|
|||||||
.as_secs() as i64
|
.as_secs() as i64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An invalid email address was encountered
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[error("Invalid email address: {message} ({addr})")]
|
||||||
|
pub struct InvalidEmailError {
|
||||||
|
message: String,
|
||||||
|
addr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InvalidEmailError {
|
||||||
|
fn new(msg: impl Into<String>, addr: impl Into<String>) -> InvalidEmailError {
|
||||||
|
InvalidEmailError {
|
||||||
|
message: msg.into(),
|
||||||
|
addr: addr.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Very simple email address wrapper.
|
/// Very simple email address wrapper.
|
||||||
///
|
///
|
||||||
/// Represents an email address, right now just the `name@domain` portion.
|
/// Represents an email address, right now just the `name@domain` portion.
|
||||||
@@ -489,7 +514,7 @@ pub struct EmailAddress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EmailAddress {
|
impl EmailAddress {
|
||||||
pub fn new(input: &str) -> Result<Self, Error> {
|
pub fn new(input: &str) -> Result<Self, InvalidEmailError> {
|
||||||
input.parse::<EmailAddress>()
|
input.parse::<EmailAddress>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,31 +526,55 @@ impl fmt::Display for EmailAddress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for EmailAddress {
|
impl FromStr for EmailAddress {
|
||||||
type Err = Error;
|
type Err = InvalidEmailError;
|
||||||
|
|
||||||
/// Performs a dead-simple parse of an email address.
|
/// Performs a dead-simple parse of an email address.
|
||||||
fn from_str(input: &str) -> Result<EmailAddress, Error> {
|
fn from_str(input: &str) -> Result<EmailAddress, InvalidEmailError> {
|
||||||
ensure!(!input.is_empty(), "empty string is not valid");
|
if input.is_empty() {
|
||||||
|
return Err(InvalidEmailError::new("empty string is not valid", input));
|
||||||
|
}
|
||||||
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
||||||
|
|
||||||
ensure!(parts.len() > 1, "missing '@' character");
|
let err = |msg: &str| {
|
||||||
let local = parts[1];
|
Err(InvalidEmailError {
|
||||||
let domain = parts[0];
|
message: msg.to_string(),
|
||||||
|
addr: input.to_string(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
match &parts[..] {
|
||||||
|
[domain, local] => {
|
||||||
|
if local.is_empty() {
|
||||||
|
return err("empty string is not valid for local part");
|
||||||
|
}
|
||||||
|
if domain.len() <= 3 {
|
||||||
|
return err("domain is too short");
|
||||||
|
}
|
||||||
|
let dot = domain.find('.');
|
||||||
|
match dot {
|
||||||
|
None => {
|
||||||
|
return err("invalid domain");
|
||||||
|
}
|
||||||
|
Some(dot_idx) => {
|
||||||
|
if dot_idx >= domain.len() - 2 {
|
||||||
|
return err("invalid domain");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(EmailAddress {
|
||||||
|
local: (*local).to_string(),
|
||||||
|
domain: (*domain).to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => err("missing '@' character"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ensure!(
|
impl rusqlite::types::ToSql for EmailAddress {
|
||||||
!local.is_empty(),
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||||
"empty string is not valid for local part"
|
let val = rusqlite::types::Value::Text(self.to_string());
|
||||||
);
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||||
ensure!(domain.len() > 3, "domain is too short");
|
Ok(out)
|
||||||
|
|
||||||
let dot = domain.find('.');
|
|
||||||
ensure!(dot.is_some(), "invalid domain");
|
|
||||||
ensure!(dot.unwrap() < domain.len() - 2, "invalid domain");
|
|
||||||
|
|
||||||
Ok(EmailAddress {
|
|
||||||
local: local.to_string(),
|
|
||||||
domain: domain.to_string(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,4 +882,37 @@ mod tests {
|
|||||||
let next = dc_smeared_time(&t.ctx).await;
|
let next = dc_smeared_time(&t.ctx).await;
|
||||||
assert!((start + count - 1) < next);
|
assert!((start + count - 1) < next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duration_to_str() {
|
||||||
|
assert_eq!(duration_to_str(Duration::from_secs(0)), "0h 0m 0s");
|
||||||
|
assert_eq!(duration_to_str(Duration::from_secs(59)), "0h 0m 59s");
|
||||||
|
assert_eq!(duration_to_str(Duration::from_secs(60)), "0h 1m 0s");
|
||||||
|
assert_eq!(duration_to_str(Duration::from_secs(61)), "0h 1m 1s");
|
||||||
|
assert_eq!(duration_to_str(Duration::from_secs(59 * 60)), "0h 59m 0s");
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(59 * 60 + 59)),
|
||||||
|
"0h 59m 59s"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(59 * 60 + 60)),
|
||||||
|
"1h 0m 0s"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 59)),
|
||||||
|
"2h 59m 59s"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 60)),
|
||||||
|
"3h 0m 0s"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(3 * 60 * 60 + 59)),
|
||||||
|
"3h 0m 59s"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
duration_to_str(Duration::from_secs(3 * 60 * 60 + 60)),
|
||||||
|
"3h 1m 0s"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
//! A module to remove HTML tags from the email text
|
//! A module to remove HTML tags from the email text
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use quick_xml;
|
|
||||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -35,6 +34,7 @@ pub fn dehtml(buf: &str) -> String {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut reader = quick_xml::Reader::from_str(buf);
|
let mut reader = quick_xml::Reader::from_str(buf);
|
||||||
|
reader.check_end_names(false);
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
@@ -225,4 +225,23 @@ mod tests {
|
|||||||
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
|
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unclosed_tags() {
|
||||||
|
let input = r##"
|
||||||
|
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'
|
||||||
|
'http://www.w3.org/TR/html4/loose.dtd'>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Hi</title>
|
||||||
|
<meta http-equiv='Content-Type' content='text/html; charset=iso-8859-1'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
lots of text
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"##;
|
||||||
|
let txt = dehtml(input);
|
||||||
|
assert_eq!(txt.trim(), "lots of text");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
src/e2ee.rs
102
src/e2ee.rs
@@ -1,19 +1,17 @@
|
|||||||
//! End-to-end encryption support.
|
//! End-to-end encryption support.
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
use mailparse::ParsedMail;
|
use mailparse::ParsedMail;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
|
|
||||||
use crate::aheader::*;
|
use crate::aheader::*;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::KeyGenType;
|
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::EmailAddress;
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
use crate::headerdef::HeaderDef;
|
||||||
use crate::key::{self, Key, KeyPairUse, SignedPublicKey};
|
use crate::headerdef::HeaderDefMap;
|
||||||
|
use crate::key::{DcKey, Key, SignedPublicKey, SignedSecretKey};
|
||||||
use crate::keyring::*;
|
use crate::keyring::*;
|
||||||
use crate::peerstate::*;
|
use crate::peerstate::*;
|
||||||
use crate::pgp;
|
use crate::pgp;
|
||||||
@@ -38,7 +36,7 @@ impl EncryptHelper {
|
|||||||
Some(addr) => addr,
|
Some(addr) => addr,
|
||||||
};
|
};
|
||||||
|
|
||||||
let public_key = load_or_generate_self_public_key(context, &addr).await?;
|
let public_key = SignedPublicKey::load_self(context).await?;
|
||||||
|
|
||||||
Ok(EncryptHelper {
|
Ok(EncryptHelper {
|
||||||
prefer_encrypt,
|
prefer_encrypt,
|
||||||
@@ -108,9 +106,7 @@ impl EncryptHelper {
|
|||||||
}
|
}
|
||||||
let public_key = Key::from(self.public_key.clone());
|
let public_key = Key::from(self.public_key.clone());
|
||||||
keyring.add_ref(&public_key);
|
keyring.add_ref(&public_key);
|
||||||
let sign_key = Key::from_self_private(context, self.addr.clone(), &context.sql)
|
let sign_key = Key::from(SignedSecretKey::load_self(context).await?);
|
||||||
.await
|
|
||||||
.ok_or_else(|| format_err!("missing own private key"))?;
|
|
||||||
|
|
||||||
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
|
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
|
||||||
|
|
||||||
@@ -127,8 +123,8 @@ pub async fn try_decrypt(
|
|||||||
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
|
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
|
||||||
let from = mail
|
let from = mail
|
||||||
.headers
|
.headers
|
||||||
.get_header_value(HeaderDef::From_)
|
.get_header(HeaderDef::From_)
|
||||||
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
|
.and_then(|from_addr| mailparse::addrparse_header(&from_addr).ok())
|
||||||
.and_then(|from| from.extract_single_info())
|
.and_then(|from| from.extract_single_info())
|
||||||
.map(|from| from.addr)
|
.map(|from| from.addr)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
@@ -193,43 +189,6 @@ pub async fn try_decrypt(
|
|||||||
Ok((out_mail, signatures))
|
Ok((out_mail, signatures))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load public key from database or generate a new one.
|
|
||||||
///
|
|
||||||
/// This will load a public key from the database, generating and
|
|
||||||
/// storing a new one when one doesn't exist yet. Care is taken to
|
|
||||||
/// only generate one key per context even when multiple threads call
|
|
||||||
/// this function concurrently.
|
|
||||||
async fn load_or_generate_self_public_key(
|
|
||||||
context: &Context,
|
|
||||||
self_addr: impl AsRef<str>,
|
|
||||||
) -> Result<SignedPublicKey> {
|
|
||||||
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql).await {
|
|
||||||
return SignedPublicKey::try_from(key)
|
|
||||||
.map_err(|_| Error::Message("Not a public key".into()));
|
|
||||||
}
|
|
||||||
let _guard = context.generating_key_mutex.lock().await;
|
|
||||||
|
|
||||||
// Check again in case the key was generated while we were waiting for the lock.
|
|
||||||
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql).await {
|
|
||||||
return SignedPublicKey::try_from(key)
|
|
||||||
.map_err(|_| Error::Message("Not a public key".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
|
|
||||||
let keygen_type =
|
|
||||||
KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await).unwrap_or_default();
|
|
||||||
info!(context, "Generating keypair with type {}", keygen_type);
|
|
||||||
let keypair = pgp::create_keypair(EmailAddress::new(self_addr.as_ref())?, keygen_type)?;
|
|
||||||
key::store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
|
|
||||||
info!(
|
|
||||||
context,
|
|
||||||
"Keypair generated in {:.3}s.",
|
|
||||||
start.elapsed().as_secs()
|
|
||||||
);
|
|
||||||
Ok(keypair.public)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
|
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
|
||||||
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
|
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
|
||||||
ensure!(
|
ensure!(
|
||||||
@@ -351,6 +310,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
|
|||||||
///
|
///
|
||||||
/// If this succeeds you are also guaranteed that the
|
/// If this succeeds you are also guaranteed that the
|
||||||
/// [Config::ConfiguredAddr] is configured, this address is returned.
|
/// [Config::ConfiguredAddr] is configured, this address is returned.
|
||||||
|
// TODO, remove this once deltachat::key::Key no longer exists.
|
||||||
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||||
let self_addr = context
|
let self_addr = context
|
||||||
.get_config(Config::ConfiguredAddr)
|
.get_config(Config::ConfiguredAddr)
|
||||||
@@ -361,8 +321,7 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
|||||||
"cannot ensure secret key if not configured."
|
"cannot ensure secret key if not configured."
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
load_or_generate_self_public_key(context, &self_addr).await?;
|
SignedPublicKey::load_self(context).await?;
|
||||||
|
|
||||||
Ok(self_addr)
|
Ok(self_addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,49 +372,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
mod load_or_generate_self_public_key {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[async_std::test]
|
|
||||||
async fn test_existing() {
|
|
||||||
let t = dummy_context().await;
|
|
||||||
let addr = configure_alice_keypair(&t.ctx).await;
|
|
||||||
let key = load_or_generate_self_public_key(&t.ctx, addr).await;
|
|
||||||
assert!(key.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_std::test]
|
|
||||||
async fn test_generate() {
|
|
||||||
let t = dummy_context().await;
|
|
||||||
let addr = "alice@example.org";
|
|
||||||
let key0 = load_or_generate_self_public_key(&t.ctx, addr).await;
|
|
||||||
assert!(key0.is_ok());
|
|
||||||
let key1 = load_or_generate_self_public_key(&t.ctx, addr).await;
|
|
||||||
assert!(key1.is_ok());
|
|
||||||
assert_eq!(key0.unwrap(), key1.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_std::test]
|
|
||||||
async fn test_generate_concurrent() {
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
let t = dummy_context().await;
|
|
||||||
let ctx = Arc::new(t.ctx);
|
|
||||||
let ctx0 = Arc::clone(&ctx);
|
|
||||||
let thr0 = async_std::task::spawn(async move {
|
|
||||||
load_or_generate_self_public_key(&ctx0, "alice@example.org").await
|
|
||||||
});
|
|
||||||
let ctx1 = Arc::clone(&ctx);
|
|
||||||
let thr1 = async_std::task::spawn(async move {
|
|
||||||
load_or_generate_self_public_key(&ctx1, "alice@example.org").await
|
|
||||||
});
|
|
||||||
|
|
||||||
let res0 = thr0.await;
|
|
||||||
let res1 = thr1.await;
|
|
||||||
assert_eq!(res0.unwrap(), res1.unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_has_decrypted_pgp_armor() {
|
fn test_has_decrypted_pgp_armor() {
|
||||||
let data = b" -----BEGIN PGP MESSAGE-----";
|
let data = b" -----BEGIN PGP MESSAGE-----";
|
||||||
|
|||||||
194
src/error.rs
194
src/error.rs
@@ -1,198 +1,6 @@
|
|||||||
//! # Error handling
|
//! # Error handling
|
||||||
|
|
||||||
use lettre_email::mime;
|
pub use anyhow::{bail, ensure, format_err, Error, Result};
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum Error {
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
Failure(failure::Error),
|
|
||||||
|
|
||||||
#[fail(display = "SQL error: {:?}", _0)]
|
|
||||||
SqlError(#[cause] crate::sql::Error),
|
|
||||||
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
Io(std::io::Error),
|
|
||||||
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
Message(String),
|
|
||||||
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
MessageWithCause(String, #[cause] failure::Error, failure::Backtrace),
|
|
||||||
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
Image(image_meta::ImageError),
|
|
||||||
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
Utf8(std::str::Utf8Error),
|
|
||||||
|
|
||||||
#[fail(display = "PGP: {:?}", _0)]
|
|
||||||
Pgp(pgp::errors::Error),
|
|
||||||
|
|
||||||
#[fail(display = "Base64Decode: {:?}", _0)]
|
|
||||||
Base64Decode(base64::DecodeError),
|
|
||||||
|
|
||||||
#[fail(display = "{:?}", _0)]
|
|
||||||
FromUtf8(std::string::FromUtf8Error),
|
|
||||||
|
|
||||||
#[fail(display = "{}", _0)]
|
|
||||||
BlobError(#[cause] crate::blob::BlobError),
|
|
||||||
|
|
||||||
#[fail(display = "Invalid Message ID.")]
|
|
||||||
InvalidMsgId,
|
|
||||||
|
|
||||||
#[fail(display = "Watch folder not found {:?}", _0)]
|
|
||||||
WatchFolderNotFound(String),
|
|
||||||
|
|
||||||
#[fail(display = "Invalid Email: {:?}", _0)]
|
|
||||||
MailParseError(#[cause] mailparse::MailParseError),
|
|
||||||
|
|
||||||
#[fail(display = "Building invalid Email: {:?}", _0)]
|
|
||||||
LettreError(#[cause] lettre_email::error::Error),
|
|
||||||
|
|
||||||
#[fail(display = "SMTP error: {:?}", _0)]
|
|
||||||
SmtpError(#[cause] async_smtp::error::Error),
|
|
||||||
|
|
||||||
#[fail(display = "FromStr error: {:?}", _0)]
|
|
||||||
FromStr(#[cause] mime::FromStrError),
|
|
||||||
|
|
||||||
#[fail(display = "Not Configured")]
|
|
||||||
NotConfigured,
|
|
||||||
|
|
||||||
#[fail(display = "No event available")]
|
|
||||||
PopError(crossbeam_queue::PopError),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
impl From<crate::sql::Error> for Error {
|
|
||||||
fn from(err: crate::sql::Error) -> Error {
|
|
||||||
Error::SqlError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crossbeam_queue::PopError> for Error {
|
|
||||||
fn from(err: crossbeam_queue::PopError) -> Error {
|
|
||||||
Error::PopError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<base64::DecodeError> for Error {
|
|
||||||
fn from(err: base64::DecodeError) -> Error {
|
|
||||||
Error::Base64Decode(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<failure::Error> for Error {
|
|
||||||
fn from(err: failure::Error) -> Error {
|
|
||||||
Error::Failure(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::io::Error> for Error {
|
|
||||||
fn from(err: std::io::Error) -> Error {
|
|
||||||
Error::Io(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::str::Utf8Error> for Error {
|
|
||||||
fn from(err: std::str::Utf8Error) -> Error {
|
|
||||||
Error::Utf8(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<image_meta::ImageError> for Error {
|
|
||||||
fn from(err: image_meta::ImageError) -> Error {
|
|
||||||
Error::Image(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<pgp::errors::Error> for Error {
|
|
||||||
fn from(err: pgp::errors::Error) -> Error {
|
|
||||||
Error::Pgp(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::string::FromUtf8Error> for Error {
|
|
||||||
fn from(err: std::string::FromUtf8Error) -> Error {
|
|
||||||
Error::FromUtf8(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::blob::BlobError> for Error {
|
|
||||||
fn from(err: crate::blob::BlobError) -> Error {
|
|
||||||
Error::BlobError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::message::InvalidMsgId> for Error {
|
|
||||||
fn from(_err: crate::message::InvalidMsgId) -> Error {
|
|
||||||
Error::InvalidMsgId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::key::SaveKeyError> for Error {
|
|
||||||
fn from(err: crate::key::SaveKeyError) -> Error {
|
|
||||||
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::pgp::PgpKeygenError> for Error {
|
|
||||||
fn from(err: crate::pgp::PgpKeygenError) -> Error {
|
|
||||||
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<mailparse::MailParseError> for Error {
|
|
||||||
fn from(err: mailparse::MailParseError) -> Error {
|
|
||||||
Error::MailParseError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<lettre_email::error::Error> for Error {
|
|
||||||
fn from(err: lettre_email::error::Error) -> Error {
|
|
||||||
Error::LettreError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<mime::FromStrError> for Error {
|
|
||||||
fn from(err: mime::FromStrError) -> Error {
|
|
||||||
Error::FromStr(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! bail {
|
|
||||||
($e:expr) => {
|
|
||||||
return Err($crate::error::Error::Message($e.to_string()));
|
|
||||||
};
|
|
||||||
($fmt:expr, $($arg:tt)+) => {
|
|
||||||
return Err($crate::error::Error::Message(format!($fmt, $($arg)+)));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! format_err {
|
|
||||||
($e:expr) => {
|
|
||||||
$crate::error::Error::Message($e.to_string());
|
|
||||||
};
|
|
||||||
($fmt:expr, $($arg:tt)+) => {
|
|
||||||
$crate::error::Error::Message(format!($fmt, $($arg)+));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export(local_inner_macros)]
|
|
||||||
macro_rules! ensure {
|
|
||||||
($cond:expr, $e:expr) => {
|
|
||||||
if !($cond) {
|
|
||||||
bail!($e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($cond:expr, $fmt:expr, $($arg:tt)+) => {
|
|
||||||
if !($cond) {
|
|
||||||
bail!($fmt, $($arg)+);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! ensure_eq {
|
macro_rules! ensure_eq {
|
||||||
|
|||||||
@@ -201,10 +201,4 @@ pub enum Event {
|
|||||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||||
#[strum(props(id = "2061"))]
|
#[strum(props(id = "2061"))]
|
||||||
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||||
|
|
||||||
/// This event is sent out to the inviter when a joiner successfully joined a group.
|
|
||||||
/// @param data1 (int) chat_id
|
|
||||||
/// @param data2 (int) contact_id
|
|
||||||
#[strum(props(id = "2062"))]
|
|
||||||
SecurejoinMemberAdded { chat_id: ChatId, contact_id: u32 },
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,12 +53,16 @@ impl HeaderDef {
|
|||||||
|
|
||||||
pub trait HeaderDefMap {
|
pub trait HeaderDefMap {
|
||||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
|
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
|
||||||
|
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HeaderDefMap for [MailHeader<'_>] {
|
impl HeaderDefMap for [MailHeader<'_>] {
|
||||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
|
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
|
||||||
self.get_first_value(headerdef.get_headername())
|
self.get_first_value(headerdef.get_headername())
|
||||||
}
|
}
|
||||||
|
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader> {
|
||||||
|
self.get_first_header(headerdef.get_headername())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -11,28 +11,22 @@ use super::session::Session;
|
|||||||
|
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[fail(display = "IMAP IDLE protocol failed to init/complete")]
|
#[error("IMAP IDLE protocol failed to init/complete")]
|
||||||
IdleProtocolFailed(#[cause] async_imap::error::Error),
|
IdleProtocolFailed(#[from] async_imap::error::Error),
|
||||||
|
|
||||||
#[fail(display = "IMAP IDLE protocol timed out")]
|
#[error("IMAP IDLE protocol timed out")]
|
||||||
IdleTimeout(#[cause] async_std::future::TimeoutError),
|
IdleTimeout(#[from] async_std::future::TimeoutError),
|
||||||
|
|
||||||
#[fail(display = "IMAP server does not have IDLE capability")]
|
#[error("IMAP server does not have IDLE capability")]
|
||||||
IdleAbilityMissing,
|
IdleAbilityMissing,
|
||||||
|
|
||||||
#[fail(display = "IMAP select folder error")]
|
#[error("IMAP select folder error")]
|
||||||
SelectFolderError(#[cause] select_folder::Error),
|
SelectFolderError(#[from] select_folder::Error),
|
||||||
|
|
||||||
#[fail(display = "Setup handle error")]
|
#[error("Setup handle error")]
|
||||||
SetupHandleError(#[cause] super::Error),
|
SetupHandleError(#[from] super::Error),
|
||||||
}
|
|
||||||
|
|
||||||
impl From<select_folder::Error> for Error {
|
|
||||||
fn from(err: select_folder::Error) -> Error {
|
|
||||||
Error::SelectFolderError(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Imap {
|
impl Imap {
|
||||||
@@ -46,10 +40,7 @@ impl Imap {
|
|||||||
if !self.can_idle() {
|
if !self.can_idle() {
|
||||||
return Err(Error::IdleAbilityMissing);
|
return Err(Error::IdleAbilityMissing);
|
||||||
}
|
}
|
||||||
|
self.setup_handle_if_needed(context).await?;
|
||||||
self.setup_handle_if_needed(context)
|
|
||||||
.await
|
|
||||||
.map_err(Error::SetupHandleError)?;
|
|
||||||
|
|
||||||
self.select_folder(context, watch_folder.clone()).await?;
|
self.select_folder(context, watch_folder.clone()).await?;
|
||||||
|
|
||||||
|
|||||||
107
src/imap/mod.rs
107
src/imap/mod.rs
@@ -23,6 +23,7 @@ use crate::headerdef::{HeaderDef, HeaderDefMap};
|
|||||||
use crate::job::{self, Action};
|
use crate::job::{self, Action};
|
||||||
use crate::login_param::{CertificateChecks, LoginParam};
|
use crate::login_param::{CertificateChecks, LoginParam};
|
||||||
use crate::message::{self, update_server_uid};
|
use crate::message::{self, update_server_uid};
|
||||||
|
use crate::mimeparser;
|
||||||
use crate::oauth2::dc_get_oauth2_access_token;
|
use crate::oauth2::dc_get_oauth2_access_token;
|
||||||
use crate::param::Params;
|
use crate::param::Params;
|
||||||
use crate::stock::StockMessage;
|
use crate::stock::StockMessage;
|
||||||
@@ -37,78 +38,48 @@ use session::Session;
|
|||||||
|
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[fail(display = "IMAP Connect without configured params")]
|
#[error("IMAP Connect without configured params")]
|
||||||
ConnectWithoutConfigure,
|
ConnectWithoutConfigure,
|
||||||
|
|
||||||
#[fail(display = "IMAP Connection Failed params: {}", _0)]
|
#[error("IMAP Connection Failed params: {0}")]
|
||||||
ConnectionFailed(String),
|
ConnectionFailed(String),
|
||||||
|
|
||||||
#[fail(display = "IMAP No Connection established")]
|
#[error("IMAP No Connection established")]
|
||||||
NoConnection,
|
NoConnection,
|
||||||
|
|
||||||
#[fail(display = "IMAP Could not get OAUTH token")]
|
#[error("IMAP Could not get OAUTH token")]
|
||||||
OauthError,
|
OauthError,
|
||||||
|
|
||||||
#[fail(display = "IMAP Could not login as {}", _0)]
|
#[error("IMAP Could not login as {0}")]
|
||||||
LoginFailed(String),
|
LoginFailed(String),
|
||||||
|
|
||||||
#[fail(display = "IMAP Could not fetch")]
|
#[error("IMAP Could not fetch")]
|
||||||
FetchFailed(#[cause] async_imap::error::Error),
|
FetchFailed(#[from] async_imap::error::Error),
|
||||||
|
|
||||||
#[fail(display = "IMAP operation attempted while it is torn down")]
|
#[error("IMAP operation attempted while it is torn down")]
|
||||||
InTeardown,
|
InTeardown,
|
||||||
|
|
||||||
#[fail(display = "IMAP operation attempted while it is torn down")]
|
#[error("IMAP operation attempted while it is torn down")]
|
||||||
SqlError(#[cause] crate::sql::Error),
|
SqlError(#[from] crate::sql::Error),
|
||||||
|
|
||||||
#[fail(display = "IMAP got error from elsewhere")]
|
#[error("IMAP got error from elsewhere")]
|
||||||
WrappedError(#[cause] crate::error::Error),
|
WrappedError(#[from] crate::error::Error),
|
||||||
|
|
||||||
#[fail(display = "IMAP select folder error")]
|
#[error("IMAP select folder error")]
|
||||||
SelectFolderError(#[cause] select_folder::Error),
|
SelectFolderError(#[from] select_folder::Error),
|
||||||
|
|
||||||
#[fail(display = "Mail parse error")]
|
#[error("Mail parse error")]
|
||||||
MailParseError(#[cause] mailparse::MailParseError),
|
MailParseError(#[from] mailparse::MailParseError),
|
||||||
|
|
||||||
#[fail(display = "No mailbox selected, folder: {:?}", _0)]
|
#[error("No mailbox selected, folder: {0}")]
|
||||||
NoMailbox(String),
|
NoMailbox(String),
|
||||||
|
|
||||||
#[fail(display = "IMAP other error: {:?}", _0)]
|
#[error("IMAP other error: {0}")]
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<crate::sql::Error> for Error {
|
|
||||||
fn from(err: crate::sql::Error) -> Error {
|
|
||||||
Error::SqlError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::error::Error> for Error {
|
|
||||||
fn from(err: crate::error::Error) -> Error {
|
|
||||||
Error::WrappedError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Error> for crate::error::Error {
|
|
||||||
fn from(err: Error) -> crate::error::Error {
|
|
||||||
crate::error::Error::Message(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<select_folder::Error> for Error {
|
|
||||||
fn from(err: select_folder::Error) -> Error {
|
|
||||||
Error::SelectFolderError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<mailparse::MailParseError> for Error {
|
|
||||||
fn from(err: mailparse::MailParseError) -> Error {
|
|
||||||
Error::MailParseError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ImapActionResult {
|
pub enum ImapActionResult {
|
||||||
Failed,
|
Failed,
|
||||||
@@ -308,7 +279,7 @@ impl Imap {
|
|||||||
context
|
context
|
||||||
.stock_string_repl_str2(
|
.stock_string_repl_str2(
|
||||||
StockMessage::ServerResponse,
|
StockMessage::ServerResponse,
|
||||||
format!("{}:{}", imap_server, imap_port),
|
format!("IMAP {}:{}", imap_server, imap_port),
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -655,8 +626,13 @@ impl Imap {
|
|||||||
read_cnt += 1;
|
read_cnt += 1;
|
||||||
let headers = get_fetch_headers(&fetch)?;
|
let headers = get_fetch_headers(&fetch)?;
|
||||||
let message_id = prefetch_get_message_id(&headers).unwrap_or_default();
|
let message_id = prefetch_get_message_id(&headers).unwrap_or_default();
|
||||||
|
if let Ok(true) = precheck_imf(context, &message_id, folder.as_ref(), cur_uid)
|
||||||
if precheck_imf(context, &message_id, folder.as_ref(), cur_uid).await {
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!(context, "precheck_imf error: {}", err);
|
||||||
|
err
|
||||||
|
})
|
||||||
|
{
|
||||||
// we know the message-id already or don't want the message otherwise.
|
// we know the message-id already or don't want the message otherwise.
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
@@ -665,6 +641,9 @@ impl Imap {
|
|||||||
folder.as_ref(),
|
folder.as_ref(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// we do not know the message-id
|
||||||
|
// or the message-id is missing (in this case, we create one in the further process)
|
||||||
|
// or some other error happened
|
||||||
let show = prefetch_should_download(context, &headers, show_emails)
|
let show = prefetch_should_download(context, &headers, show_emails)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
@@ -792,13 +771,7 @@ impl Imap {
|
|||||||
if let Err(err) =
|
if let Err(err) =
|
||||||
dc_receive_imf(context, &body, folder.as_ref(), server_uid, is_seen).await
|
dc_receive_imf(context, &body, folder.as_ref(), server_uid, is_seen).await
|
||||||
{
|
{
|
||||||
warn!(
|
return Err(Error::Other(format!("dc_receive_imf error: {}", err)));
|
||||||
context,
|
|
||||||
"dc_receive_imf failed for imap-message {}/{}: {:?}",
|
|
||||||
folder.as_ref(),
|
|
||||||
server_uid,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1324,9 +1297,9 @@ async fn precheck_imf(
|
|||||||
rfc724_mid: &str,
|
rfc724_mid: &str,
|
||||||
server_folder: &str,
|
server_folder: &str,
|
||||||
server_uid: u32,
|
server_uid: u32,
|
||||||
) -> bool {
|
) -> Result<bool> {
|
||||||
if let Ok((old_server_folder, old_server_uid, msg_id)) =
|
if let Some((old_server_folder, old_server_uid, msg_id)) =
|
||||||
message::rfc724_mid_exists(context, &rfc724_mid).await
|
message::rfc724_mid_exists(context, &rfc724_mid).await?
|
||||||
{
|
{
|
||||||
if old_server_folder.is_empty() && old_server_uid == 0 {
|
if old_server_folder.is_empty() && old_server_uid == 0 {
|
||||||
info!(
|
info!(
|
||||||
@@ -1383,9 +1356,9 @@ async fn precheck_imf(
|
|||||||
if old_server_folder != server_folder || old_server_uid != server_uid {
|
if old_server_folder != server_folder || old_server_uid != server_uid {
|
||||||
update_server_uid(context, &rfc724_mid, server_folder, server_uid).await;
|
update_server_uid(context, &rfc724_mid, server_folder, server_uid).await;
|
||||||
}
|
}
|
||||||
true
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
false
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1438,12 +1411,8 @@ async fn prefetch_should_download(
|
|||||||
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
.get_header_value(HeaderDef::AutocryptSetupMessage)
|
||||||
.is_some();
|
.is_some();
|
||||||
|
|
||||||
let from_field = headers
|
|
||||||
.get_header_value(HeaderDef::From_)
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let (_contact_id, blocked_contact, origin) =
|
let (_contact_id, blocked_contact, origin) =
|
||||||
from_field_to_contact_id(context, &from_field).await?;
|
from_field_to_contact_id(context, &mimeparser::get_from(headers)).await?;
|
||||||
let accepted_contact = origin.is_known();
|
let accepted_contact = origin.is_known();
|
||||||
|
|
||||||
let show = is_autocrypt_setup_message
|
let show = is_autocrypt_setup_message
|
||||||
|
|||||||
@@ -4,25 +4,52 @@ use crate::context::Context;
|
|||||||
|
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[fail(display = "IMAP Could not obtain imap-session object.")]
|
#[error("IMAP Could not obtain imap-session object.")]
|
||||||
NoSession,
|
NoSession,
|
||||||
|
|
||||||
#[fail(display = "IMAP Connection Lost or no connection established")]
|
#[error("IMAP Connection Lost or no connection established")]
|
||||||
ConnectionLost,
|
ConnectionLost,
|
||||||
|
|
||||||
#[fail(display = "IMAP Folder name invalid: {:?}", _0)]
|
#[error("IMAP Folder name invalid: {0}")]
|
||||||
BadFolderName(String),
|
BadFolderName(String),
|
||||||
|
|
||||||
#[fail(display = "IMAP close/expunge failed: {}", _0)]
|
#[error("IMAP close/expunge failed")]
|
||||||
CloseExpungeFailed(#[cause] async_imap::error::Error),
|
CloseExpungeFailed(#[from] async_imap::error::Error),
|
||||||
|
|
||||||
#[fail(display = "IMAP other error: {:?}", _0)]
|
#[error("IMAP other error: {0}")]
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Imap {
|
impl Imap {
|
||||||
|
/// Issues a CLOSE command to expunge selected folder.
|
||||||
|
///
|
||||||
|
/// CLOSE is considerably faster than an EXPUNGE, see
|
||||||
|
/// https://tools.ietf.org/html/rfc3501#section-6.4.2
|
||||||
|
async fn close_folder(&mut self, context: &Context) -> Result<()> {
|
||||||
|
if let Some(ref folder) = self.config.selected_folder {
|
||||||
|
info!(context, "Expunge messages in \"{}\".", folder);
|
||||||
|
|
||||||
|
if let Some(ref mut session) = self.session {
|
||||||
|
match session.close().await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!(context, "close/expunge succeeded");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.trigger_reconnect();
|
||||||
|
return Err(Error::CloseExpungeFailed(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::NoSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.config.selected_folder = None;
|
||||||
|
self.config.selected_folder_needs_expunge = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// select a folder, possibly update uid_validity and, if needed,
|
/// select a folder, possibly update uid_validity and, if needed,
|
||||||
/// expunge the folder to remove delete-marked messages.
|
/// expunge the folder to remove delete-marked messages.
|
||||||
pub(super) async fn select_folder<S: AsRef<str>>(
|
pub(super) async fn select_folder<S: AsRef<str>>(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use crate::dc_tools::*;
|
|||||||
use crate::e2ee;
|
use crate::e2ee;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
use crate::key::{self, Key};
|
use crate::key::{self, DcKey, Key, SignedSecretKey};
|
||||||
use crate::message::{Message, MsgId};
|
use crate::message::{Message, MsgId};
|
||||||
use crate::mimeparser::SystemMessage;
|
use crate::mimeparser::SystemMessage;
|
||||||
use crate::param::*;
|
use crate::param::*;
|
||||||
@@ -181,10 +181,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
|
|||||||
passphrase.len() >= 2,
|
passphrase.len() >= 2,
|
||||||
"Passphrase must be at least 2 chars long."
|
"Passphrase must be at least 2 chars long."
|
||||||
);
|
);
|
||||||
let self_addr = e2ee::ensure_secret_key_exists(context).await?;
|
let private_key = Key::from(SignedSecretKey::load_self(context).await?);
|
||||||
let private_key = Key::from_self_private(context, self_addr, &context.sql)
|
|
||||||
.await
|
|
||||||
.ok_or_else(|| format_err!("Failed to get private key."))?;
|
|
||||||
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
|
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
|
||||||
false => None,
|
false => None,
|
||||||
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
|
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
|
||||||
|
|||||||
233
src/job.rs
233
src/job.rs
@@ -10,6 +10,10 @@ use deltachat_derive::{FromSql, ToSql};
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
|
use async_smtp::smtp::response::Category;
|
||||||
|
use async_smtp::smtp::response::Code;
|
||||||
|
use async_smtp::smtp::response::Detail;
|
||||||
|
|
||||||
use crate::blob::BlobObject;
|
use crate::blob::BlobObject;
|
||||||
use crate::chat::{self, ChatId};
|
use crate::chat::{self, ChatId};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
@@ -17,7 +21,7 @@ use crate::constants::*;
|
|||||||
use crate::contact::Contact;
|
use crate::contact::Contact;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{bail, ensure, format_err, Error, Result};
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
use crate::imap::*;
|
use crate::imap::*;
|
||||||
use crate::location;
|
use crate::location;
|
||||||
@@ -70,7 +74,19 @@ impl Default for Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Display,
|
||||||
|
Copy,
|
||||||
|
Clone,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
PartialOrd,
|
||||||
|
FromPrimitive,
|
||||||
|
ToPrimitive,
|
||||||
|
FromSql,
|
||||||
|
ToSql,
|
||||||
|
)]
|
||||||
#[repr(i32)]
|
#[repr(i32)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
@@ -140,32 +156,68 @@ impl fmt::Display for Job {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Job {
|
impl Job {
|
||||||
/// Deletes the job from the database.
|
fn new(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Self {
|
||||||
async fn delete(&self, context: &Context) -> bool {
|
let timestamp = time();
|
||||||
context
|
|
||||||
.sql
|
Self {
|
||||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
|
job_id: 0,
|
||||||
.await
|
action,
|
||||||
.is_ok()
|
foreign_id,
|
||||||
|
desired_timestamp: timestamp + delay_seconds,
|
||||||
|
added_timestamp: timestamp,
|
||||||
|
tries: 0,
|
||||||
|
param,
|
||||||
|
pending_error: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the job already stored in the database.
|
/// Deletes the job from the database.
|
||||||
|
async fn delete(&self, context: &Context) -> bool {
|
||||||
|
if self.job_id != 0 {
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
} else {
|
||||||
|
// Already deleted.
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves the job to the database, creating a new entry if necessary.
|
||||||
///
|
///
|
||||||
/// To add a new job, use [job_add].
|
/// The Job is consumed by this method.
|
||||||
async fn update(&self, context: &Context) -> bool {
|
async fn save(self, context: &Context) -> bool {
|
||||||
context
|
let thread: Thread = self.action.into();
|
||||||
.sql
|
|
||||||
.execute(
|
if self.job_id != 0 {
|
||||||
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
|
||||||
|
paramsv![
|
||||||
|
self.desired_timestamp,
|
||||||
|
self.tries as i64,
|
||||||
|
self.param.to_string(),
|
||||||
|
self.job_id as i32,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
} else {
|
||||||
|
context.sql.execute(
|
||||||
|
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
|
||||||
paramsv![
|
paramsv![
|
||||||
self.desired_timestamp,
|
self.added_timestamp,
|
||||||
self.tries as i64,
|
thread,
|
||||||
|
self.action,
|
||||||
|
self.foreign_id,
|
||||||
self.param.to_string(),
|
self.param.to_string(),
|
||||||
self.job_id as i32,
|
self.desired_timestamp
|
||||||
],
|
]
|
||||||
)
|
).await.is_ok()
|
||||||
.await
|
}
|
||||||
.is_ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn smtp_send<F, Fut>(
|
async fn smtp_send<F, Fut>(
|
||||||
@@ -196,8 +248,42 @@ impl Job {
|
|||||||
self.pending_error = Some(err.to_string());
|
self.pending_error = Some(err.to_string());
|
||||||
|
|
||||||
let res = match err {
|
let res = match err {
|
||||||
async_smtp::smtp::error::Error::Permanent(_) => {
|
async_smtp::smtp::error::Error::Permanent(ref response) => {
|
||||||
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
match response.code {
|
||||||
|
// Sometimes servers send a permanent error when actually it is a temporary error
|
||||||
|
// For documentation see https://tools.ietf.org/html/rfc3463
|
||||||
|
|
||||||
|
// Code 5.5.0, see https://support.delta.chat/t/every-other-message-gets-stuck/877/2
|
||||||
|
Code {
|
||||||
|
category: Category::MailSystem,
|
||||||
|
detail: Detail::Zero,
|
||||||
|
..
|
||||||
|
} => Status::RetryLater,
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
// If we do not retry, add an info message to the chat
|
||||||
|
// Error 5.7.1 should definitely go here: Yandex sends 5.7.1 with a link when it thinks that the email is SPAM.
|
||||||
|
match Message::load_from_db(context, MsgId::new(self.foreign_id))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(message) => {
|
||||||
|
chat::add_info_msg(
|
||||||
|
context,
|
||||||
|
message.chat_id,
|
||||||
|
err.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Err(e) => warn!(
|
||||||
|
context,
|
||||||
|
"couldn't load chat_id to inform user about SMTP error: {}",
|
||||||
|
e
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async_smtp::smtp::error::Error::Transient(_) => {
|
async_smtp::smtp::error::Error::Transient(_) => {
|
||||||
// We got a transient 4xx response from SMTP server.
|
// We got a transient 4xx response from SMTP server.
|
||||||
@@ -223,7 +309,7 @@ impl Job {
|
|||||||
// Local error, job is invalid, do not retry.
|
// Local error, job is invalid, do not retry.
|
||||||
smtp.disconnect().await;
|
smtp.disconnect().await;
|
||||||
warn!(context, "SMTP job is invalid: {}", err);
|
warn!(context, "SMTP job is invalid: {}", err);
|
||||||
Status::Finished(Err(Error::SmtpError(err)))
|
Status::Finished(Err(err.into()))
|
||||||
}
|
}
|
||||||
Err(crate::smtp::send::Error::NoTransport) => {
|
Err(crate::smtp::send::Error::NoTransport) => {
|
||||||
// Should never happen.
|
// Should never happen.
|
||||||
@@ -756,13 +842,46 @@ async fn add_imap_deletion_jobs(context: &Context) -> sql::Result<()> {
|
|||||||
Params::new(),
|
Params::new(),
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
|
||||||
|
if let Some(delete_server_after) = context.get_config_delete_server_after().await {
|
||||||
|
let threshold_timestamp = time() - delete_server_after;
|
||||||
|
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.query_row_optional(
|
||||||
|
"SELECT id FROM msgs \
|
||||||
|
WHERE timestamp < ? \
|
||||||
|
AND server_uid != 0",
|
||||||
|
paramsv![threshold_timestamp],
|
||||||
|
|row| row.get::<_, MsgId>(0),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_imap_deletion_job(context: &Context) -> sql::Result<Option<Job>> {
|
||||||
|
let res = if let Some(msg_id) = load_imap_deletion_msgid(context).await? {
|
||||||
|
Some(Job::new(
|
||||||
|
Action::DeleteMsgOnImap,
|
||||||
|
msg_id.to_u32(),
|
||||||
|
Params::new(),
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> fmt::Display for Connection<'a> {
|
impl<'a> fmt::Display for Connection<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@@ -808,7 +927,6 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
|||||||
job.tries = tries;
|
job.tries = tries;
|
||||||
let time_offset = get_backoff_time_offset(tries);
|
let time_offset = get_backoff_time_offset(tries);
|
||||||
job.desired_timestamp = time() + time_offset;
|
job.desired_timestamp = time() + time_offset;
|
||||||
job.update(context).await;
|
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
"{}-job #{} not succeeded on try #{}, retry in {} seconds.",
|
"{}-job #{} not succeeded on try #{}, retry in {} seconds.",
|
||||||
@@ -817,6 +935,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
|||||||
tries,
|
tries,
|
||||||
time_offset
|
time_offset
|
||||||
);
|
);
|
||||||
|
job.save(context).await;
|
||||||
} else {
|
} else {
|
||||||
info!(
|
info!(
|
||||||
context,
|
context,
|
||||||
@@ -938,6 +1057,9 @@ pub async fn add(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let param_str = param.to_string();
|
||||||
|
let job = Job::new(action, foreign_id as u32, param, delay_seconds);
|
||||||
|
job.save(context).await;
|
||||||
let timestamp = time();
|
let timestamp = time();
|
||||||
let thread: Thread = action.into();
|
let thread: Thread = action.into();
|
||||||
|
|
||||||
@@ -948,26 +1070,28 @@ pub async fn add(
|
|||||||
thread,
|
thread,
|
||||||
action,
|
action,
|
||||||
foreign_id,
|
foreign_id,
|
||||||
param.to_string(),
|
param_str,
|
||||||
(timestamp + delay_seconds as i64)
|
(timestamp + delay_seconds as i64)
|
||||||
]
|
]
|
||||||
).await.ok();
|
).await.ok();
|
||||||
|
|
||||||
match action {
|
if delay_seconds == 0 {
|
||||||
Action::Unknown => unreachable!(),
|
match action {
|
||||||
Action::Housekeeping
|
Action::Unknown => unreachable!(),
|
||||||
| Action::EmptyServer
|
Action::Housekeeping
|
||||||
| Action::OldDeleteMsgOnImap
|
| Action::EmptyServer
|
||||||
| Action::DeleteMsgOnImap
|
| Action::OldDeleteMsgOnImap
|
||||||
| Action::MarkseenMsgOnImap
|
| Action::DeleteMsgOnImap
|
||||||
| Action::MoveMsg => {
|
| Action::MarkseenMsgOnImap
|
||||||
context.interrupt_inbox().await;
|
| Action::MoveMsg => {
|
||||||
}
|
context.interrupt_inbox().await;
|
||||||
Action::MaybeSendLocations
|
}
|
||||||
| Action::MaybeSendLocationsEnded
|
Action::MaybeSendLocations
|
||||||
| Action::SendMdn
|
| Action::MaybeSendLocationsEnded
|
||||||
| Action::SendMsgToSmtp => {
|
| Action::SendMdn
|
||||||
context.interrupt_smtp().await;
|
| Action::SendMsgToSmtp => {
|
||||||
|
context.interrupt_smtp().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1006,7 +1130,7 @@ pub(crate) async fn load_next(
|
|||||||
params_probe
|
params_probe
|
||||||
};
|
};
|
||||||
|
|
||||||
context
|
let job = context
|
||||||
.sql
|
.sql
|
||||||
.query_map(
|
.query_map(
|
||||||
query,
|
query,
|
||||||
@@ -1036,7 +1160,24 @@ pub(crate) async fn load_next(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if thread == Thread::Imap {
|
||||||
|
if let Some(job) = job {
|
||||||
|
if job.action < Action::DeleteMsgOnImap {
|
||||||
|
load_imap_deletion_job(context)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.or(Some(job))
|
||||||
|
} else {
|
||||||
|
Some(job)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
load_imap_deletion_job(context).await.unwrap_or_default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
job
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
239
src/key.rs
239
src/key.rs
@@ -4,38 +4,40 @@ use std::collections::BTreeMap;
|
|||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use async_std::path::Path;
|
use async_std::path::Path;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use num_traits::FromPrimitive;
|
||||||
use pgp::composed::Deserializable;
|
use pgp::composed::Deserializable;
|
||||||
use pgp::ser::Serialize;
|
use pgp::ser::Serialize;
|
||||||
use pgp::types::{KeyTrait, SecretKeyTrait};
|
use pgp::types::{KeyTrait, SecretKeyTrait};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::{dc_write_file, time, EmailAddress, InvalidEmailError};
|
||||||
use crate::sql::Sql;
|
use crate::sql;
|
||||||
|
|
||||||
// Re-export key types
|
// Re-export key types
|
||||||
pub use crate::pgp::KeyPair;
|
pub use crate::pgp::KeyPair;
|
||||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||||
|
|
||||||
/// Error type for deltachat key handling.
|
/// Error type for deltachat key handling.
|
||||||
#[derive(Fail, Debug)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[fail(display = "Could not decode base64")]
|
#[error("Could not decode base64")]
|
||||||
Base64Decode(#[cause] base64::DecodeError, failure::Backtrace),
|
Base64Decode(#[from] base64::DecodeError),
|
||||||
#[fail(display = "rPGP error: {}", _0)]
|
#[error("rPGP error: {}", _0)]
|
||||||
PgpError(#[cause] pgp::errors::Error, failure::Backtrace),
|
Pgp(#[from] pgp::errors::Error),
|
||||||
}
|
#[error("Failed to generate PGP key: {}", _0)]
|
||||||
|
Keygen(#[from] crate::pgp::PgpKeygenError),
|
||||||
impl From<base64::DecodeError> for Error {
|
#[error("Failed to load key: {}", _0)]
|
||||||
fn from(err: base64::DecodeError) -> Error {
|
LoadKey(#[from] sql::Error),
|
||||||
Error::Base64Decode(err, failure::Backtrace::new())
|
#[error("Failed to save generated key: {}", _0)]
|
||||||
}
|
StoreKey(#[from] SaveKeyError),
|
||||||
}
|
#[error("No address configured")]
|
||||||
|
NoConfiguredAddr,
|
||||||
impl From<pgp::errors::Error> for Error {
|
#[error("Configured address is invalid: {}", _0)]
|
||||||
fn from(err: pgp::errors::Error) -> Error {
|
InvalidConfiguredAddr(#[from] InvalidEmailError),
|
||||||
Error::PgpError(err, failure::Backtrace::new())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
@@ -45,6 +47,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||||||
/// This trait is implemented for rPGP's [SignedPublicKey] and
|
/// This trait is implemented for rPGP's [SignedPublicKey] and
|
||||||
/// [SignedSecretKey] types and makes working with them a little
|
/// [SignedSecretKey] types and makes working with them a little
|
||||||
/// easier in the deltachat world.
|
/// easier in the deltachat world.
|
||||||
|
#[async_trait]
|
||||||
pub trait DcKey: Serialize + Deserializable {
|
pub trait DcKey: Serialize + Deserializable {
|
||||||
type KeyType: Serialize + Deserializable;
|
type KeyType: Serialize + Deserializable;
|
||||||
|
|
||||||
@@ -63,6 +66,9 @@ pub trait DcKey: Serialize + Deserializable {
|
|||||||
Self::from_slice(&bytes)
|
Self::from_slice(&bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load the users' default key from the database.
|
||||||
|
async fn load_self(context: &Context) -> Result<Self::KeyType>;
|
||||||
|
|
||||||
/// Serialise the key to a base64 string.
|
/// Serialise the key to a base64 string.
|
||||||
fn to_base64(&self) -> String {
|
fn to_base64(&self) -> String {
|
||||||
// Not using Serialize::to_bytes() to make clear *why* it is
|
// Not using Serialize::to_bytes() to make clear *why* it is
|
||||||
@@ -75,12 +81,108 @@ pub trait DcKey: Serialize + Deserializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl DcKey for SignedPublicKey {
|
impl DcKey for SignedPublicKey {
|
||||||
type KeyType = SignedPublicKey;
|
type KeyType = SignedPublicKey;
|
||||||
|
|
||||||
|
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||||
|
match context
|
||||||
|
.sql
|
||||||
|
.query_row(
|
||||||
|
r#"
|
||||||
|
SELECT public_key
|
||||||
|
FROM keypairs
|
||||||
|
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||||
|
AND is_default=1;
|
||||||
|
"#,
|
||||||
|
paramsv![],
|
||||||
|
|row| row.get::<_, Vec<u8>>(0),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(bytes) => Self::from_slice(&bytes),
|
||||||
|
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||||
|
let keypair = generate_keypair(context).await?;
|
||||||
|
Ok(keypair.public)
|
||||||
|
}
|
||||||
|
Err(err) => Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl DcKey for SignedSecretKey {
|
impl DcKey for SignedSecretKey {
|
||||||
type KeyType = SignedSecretKey;
|
type KeyType = SignedSecretKey;
|
||||||
|
|
||||||
|
async fn load_self(context: &Context) -> Result<Self::KeyType> {
|
||||||
|
match context
|
||||||
|
.sql
|
||||||
|
.query_row(
|
||||||
|
r#"
|
||||||
|
SELECT private_key
|
||||||
|
FROM keypairs
|
||||||
|
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
|
||||||
|
AND is_default=1;
|
||||||
|
"#,
|
||||||
|
paramsv![],
|
||||||
|
|row| row.get::<_, Vec<u8>>(0),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(bytes) => Self::from_slice(&bytes),
|
||||||
|
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||||
|
let keypair = generate_keypair(context).await?;
|
||||||
|
Ok(keypair.secret)
|
||||||
|
}
|
||||||
|
Err(err) => Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||||
|
let addr = context
|
||||||
|
.get_config(Config::ConfiguredAddr)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| Error::NoConfiguredAddr)?;
|
||||||
|
let addr = EmailAddress::new(&addr)?;
|
||||||
|
let _guard = context.generating_key_mutex.lock().await;
|
||||||
|
|
||||||
|
// Check if the key appeared while we were waiting on the lock.
|
||||||
|
match context
|
||||||
|
.sql
|
||||||
|
.query_row(
|
||||||
|
r#"
|
||||||
|
SELECT public_key, private_key
|
||||||
|
FROM keypairs
|
||||||
|
WHERE addr=?1
|
||||||
|
AND is_default=1;
|
||||||
|
"#,
|
||||||
|
paramsv![addr],
|
||||||
|
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
|
||||||
|
addr,
|
||||||
|
public: SignedPublicKey::from_slice(&pub_bytes)?,
|
||||||
|
secret: SignedSecretKey::from_slice(&sec_bytes)?,
|
||||||
|
}),
|
||||||
|
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
|
||||||
|
.unwrap_or_default();
|
||||||
|
info!(context, "Generating keypair with type {}", keytype);
|
||||||
|
let keypair = crate::pgp::create_keypair(addr, keytype)?;
|
||||||
|
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
"Keypair generated in {:.3}s.",
|
||||||
|
start.elapsed().as_secs()
|
||||||
|
);
|
||||||
|
Ok(keypair)
|
||||||
|
}
|
||||||
|
Err(err) => Err(err.into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cryptographic key
|
/// Cryptographic key
|
||||||
@@ -197,36 +299,6 @@ impl Key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn from_self_public(
|
|
||||||
context: &Context,
|
|
||||||
self_addr: impl AsRef<str>,
|
|
||||||
sql: &Sql,
|
|
||||||
) -> Option<Self> {
|
|
||||||
let addr = self_addr.as_ref();
|
|
||||||
|
|
||||||
sql.query_get_value(
|
|
||||||
context,
|
|
||||||
"SELECT public_key FROM keypairs WHERE addr=? AND is_default=1;",
|
|
||||||
paramsv![addr],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Public))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn from_self_private(
|
|
||||||
context: &Context,
|
|
||||||
self_addr: impl AsRef<str>,
|
|
||||||
sql: &Sql,
|
|
||||||
) -> Option<Self> {
|
|
||||||
sql.query_get_value(
|
|
||||||
context,
|
|
||||||
"SELECT private_key FROM keypairs WHERE addr=? AND is_default=1;",
|
|
||||||
paramsv![self_addr.as_ref()],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Private))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
match self {
|
match self {
|
||||||
Key::Public(k) => k.to_bytes().unwrap_or_default(),
|
Key::Public(k) => k.to_bytes().unwrap_or_default(),
|
||||||
@@ -318,21 +390,19 @@ pub enum KeyPairUse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Error saving a keypair to the database.
|
/// Error saving a keypair to the database.
|
||||||
#[derive(Fail, Debug)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
#[fail(display = "SaveKeyError: {}", message)]
|
#[error("SaveKeyError: {message}")]
|
||||||
pub struct SaveKeyError {
|
pub struct SaveKeyError {
|
||||||
message: String,
|
message: String,
|
||||||
#[cause]
|
#[source]
|
||||||
cause: failure::Error,
|
cause: anyhow::Error,
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SaveKeyError {
|
impl SaveKeyError {
|
||||||
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
|
fn new(message: impl Into<String>, cause: impl Into<anyhow::Error>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
cause: cause.into(),
|
cause: cause.into(),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,6 +629,63 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_load_self_existing() {
|
||||||
|
let alice = alice_keypair();
|
||||||
|
let t = dummy_context().await;
|
||||||
|
configure_alice_keypair(&t.ctx).await;
|
||||||
|
let pubkey = SignedPublicKey::load_self(&t.ctx).await.unwrap();
|
||||||
|
assert_eq!(alice.public, pubkey);
|
||||||
|
let seckey = SignedSecretKey::load_self(&t.ctx).await.unwrap();
|
||||||
|
assert_eq!(alice.secret, seckey);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
#[ignore] // generating keys is expensive
|
||||||
|
async fn test_load_self_generate_public() {
|
||||||
|
let t = dummy_context().await;
|
||||||
|
t.ctx
|
||||||
|
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let key = SignedPublicKey::load_self(&t.ctx).await;
|
||||||
|
assert!(key.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
#[ignore] // generating keys is expensive
|
||||||
|
async fn test_load_self_generate_secret() {
|
||||||
|
let t = dummy_context().await;
|
||||||
|
t.ctx
|
||||||
|
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let key = SignedSecretKey::load_self(&t.ctx).await;
|
||||||
|
assert!(key.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
#[ignore] // generating keys is expensive
|
||||||
|
async fn test_load_self_generate_concurrent() {
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
let t = dummy_context().await;
|
||||||
|
t.ctx
|
||||||
|
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let ctx = t.ctx.clone();
|
||||||
|
let ctx0 = ctx.clone();
|
||||||
|
let thr0 =
|
||||||
|
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx0)));
|
||||||
|
let ctx1 = ctx.clone();
|
||||||
|
let thr1 =
|
||||||
|
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx1)));
|
||||||
|
let res0 = thr0.join().unwrap();
|
||||||
|
let res1 = thr1.join().unwrap();
|
||||||
|
assert_eq!(res0.unwrap(), res1.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ascii_roundtrip() {
|
fn test_ascii_roundtrip() {
|
||||||
let public_key = Key::from(KEYPAIR.public.clone());
|
let public_key = Key::from(KEYPAIR.public.clone());
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
|
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
|
||||||
#![allow(clippy::match_bool)]
|
#![allow(clippy::match_bool)]
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate failure_derive;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate num_derive;
|
extern crate num_derive;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
//! Location handling
|
//! Location handling
|
||||||
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use quick_xml;
|
|
||||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||||
|
|
||||||
use crate::chat::{self, ChatId};
|
use crate::chat::{self, ChatId};
|
||||||
@@ -9,7 +8,7 @@ use crate::config::Config;
|
|||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::context::*;
|
use crate::context::*;
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::error::Error;
|
use crate::error::{ensure, Error};
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
use crate::job::{self, Job};
|
use crate::job::{self, Job};
|
||||||
use crate::message::{Message, MsgId};
|
use crate::message::{Message, MsgId};
|
||||||
@@ -122,9 +121,9 @@ impl Kml {
|
|||||||
}
|
}
|
||||||
} else if self.tag.contains(KmlTag::COORDINATES) {
|
} else if self.tag.contains(KmlTag::COORDINATES) {
|
||||||
let parts = val.splitn(2, ',').collect::<Vec<_>>();
|
let parts = val.splitn(2, ',').collect::<Vec<_>>();
|
||||||
if parts.len() == 2 {
|
if let [longitude, latitude] = &parts[..] {
|
||||||
self.curr.longitude = parts[0].parse().unwrap_or_default();
|
self.curr.longitude = longitude.parse().unwrap_or_default();
|
||||||
self.curr.latitude = parts[1].parse().unwrap_or_default();
|
self.curr.latitude = latitude.parse().unwrap_or_default();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ impl Lot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_text1(&self) -> Option<&str> {
|
pub fn get_text1(&self) -> Option<&str> {
|
||||||
self.text1.as_ref().map(|s| s.as_str())
|
self.text1.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_text2(&self) -> Option<&str> {
|
pub fn get_text2(&self) -> Option<&str> {
|
||||||
self.text2.as_ref().map(|s| s.as_str())
|
self.text2.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_text1_meaning(&self) -> Meaning {
|
pub fn get_text1_meaning(&self) -> Meaning {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
use async_std::path::{Path, PathBuf};
|
use async_std::path::{Path, PathBuf};
|
||||||
use deltachat_derive::{FromSql, ToSql};
|
use deltachat_derive::{FromSql, ToSql};
|
||||||
use failure::Fail;
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ use crate::constants::*;
|
|||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
use crate::context::*;
|
use crate::context::*;
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::error::Error;
|
use crate::error::{ensure, Error};
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
use crate::job::{self, Action};
|
use crate::job::{self, Action};
|
||||||
use crate::lot::{Lot, LotState, Meaning};
|
use crate::lot::{Lot, LotState, Meaning};
|
||||||
@@ -169,7 +168,7 @@ impl rusqlite::types::ToSql for MsgId {
|
|||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||||
if self.0 <= DC_MSG_ID_LAST_SPECIAL {
|
if self.0 <= DC_MSG_ID_LAST_SPECIAL {
|
||||||
return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
|
return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
|
||||||
InvalidMsgId.compat(),
|
InvalidMsgId,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let val = rusqlite::types::Value::Integer(self.0 as i64);
|
let val = rusqlite::types::Value::Integer(self.0 as i64);
|
||||||
@@ -197,8 +196,8 @@ impl rusqlite::types::FromSql for MsgId {
|
|||||||
/// This usually occurs when trying to use a message ID of
|
/// This usually occurs when trying to use a message ID of
|
||||||
/// [DC_MSG_ID_LAST_SPECIAL] or below in a situation where this is not
|
/// [DC_MSG_ID_LAST_SPECIAL] or below in a situation where this is not
|
||||||
/// possible.
|
/// possible.
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
#[fail(display = "Invalid Message ID.")]
|
#[error("Invalid Message ID.")]
|
||||||
pub struct InvalidMsgId;
|
pub struct InvalidMsgId;
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
@@ -1079,9 +1078,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> bool {
|
|||||||
if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res {
|
if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let (state, blocked) = query_res
|
let (state, blocked) = query_res.map_err(Into::<anyhow::Error>::into)?;
|
||||||
.map_err(|err| Error::SqlError(err.into()))
|
|
||||||
.expect("query fail");
|
|
||||||
msgs.push((id, state, blocked));
|
msgs.push((id, state, blocked));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1261,7 +1258,7 @@ pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl
|
|||||||
}
|
}
|
||||||
if let Some(error) = error {
|
if let Some(error) = error {
|
||||||
msg.param.set(Param::Error, error.as_ref());
|
msg.param.set(Param::Error, error.as_ref());
|
||||||
error!(context, "{}", error.as_ref());
|
warn!(context, "Message failed: {}", error.as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
if context
|
if context
|
||||||
@@ -1504,12 +1501,15 @@ pub async fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 {
|
|||||||
pub(crate) async fn rfc724_mid_exists(
|
pub(crate) async fn rfc724_mid_exists(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
rfc724_mid: &str,
|
rfc724_mid: &str,
|
||||||
) -> Result<(String, u32, MsgId), Error> {
|
) -> Result<Option<(String, u32, MsgId)>, Error> {
|
||||||
ensure!(!rfc724_mid.is_empty(), "empty rfc724_mid");
|
if rfc724_mid.is_empty() {
|
||||||
|
warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
let res = context
|
let res = context
|
||||||
.sql
|
.sql
|
||||||
.query_row(
|
.query_row_optional(
|
||||||
"SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?",
|
"SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?",
|
||||||
paramsv![rfc724_mid],
|
paramsv![rfc724_mid],
|
||||||
|row| {
|
|row| {
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ use crate::contact::*;
|
|||||||
use crate::context::{get_version_str, Context};
|
use crate::context::{get_version_str, Context};
|
||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::e2ee::*;
|
use crate::e2ee::*;
|
||||||
use crate::error::Error;
|
use crate::error::{bail, ensure, format_err, Error};
|
||||||
use crate::location;
|
use crate::location;
|
||||||
use crate::message::{self, Message};
|
use crate::message::{self, Message};
|
||||||
use crate::mimeparser::SystemMessage;
|
use crate::mimeparser::SystemMessage;
|
||||||
use crate::param::*;
|
use crate::param::*;
|
||||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||||
|
use crate::simplify::escape_message_footer_marks;
|
||||||
use crate::stock::StockMessage;
|
use crate::stock::StockMessage;
|
||||||
|
|
||||||
// attachments of 25 mb brutto should work on the majority of providers
|
// attachments of 25 mb brutto should work on the majority of providers
|
||||||
@@ -113,22 +114,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
|||||||
|
|
||||||
let command = msg.param.get_cmd();
|
let command = msg.param.get_cmd();
|
||||||
|
|
||||||
/* for added members, the list is just fine */
|
|
||||||
if command == SystemMessage::MemberRemovedFromGroup {
|
|
||||||
let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default();
|
|
||||||
|
|
||||||
let self_addr = context
|
|
||||||
.get_config(Config::ConfiguredAddr)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if !email_to_remove.is_empty()
|
|
||||||
&& !addr_cmp(email_to_remove, self_addr)
|
|
||||||
&& !recipients_contain_addr(&recipients, &email_to_remove)
|
|
||||||
{
|
|
||||||
recipients.push(("".to_string(), email_to_remove.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if command != SystemMessage::AutocryptSetupMessage
|
if command != SystemMessage::AutocryptSetupMessage
|
||||||
&& command != SystemMessage::SecurejoinMessage
|
&& command != SystemMessage::SecurejoinMessage
|
||||||
&& context.get_config_bool(Config::MdnsEnabled).await
|
&& context.get_config_bool(Config::MdnsEnabled).await
|
||||||
@@ -865,7 +850,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
|
|||||||
let message_text = format!(
|
let message_text = format!(
|
||||||
"{}{}{}{}{}",
|
"{}{}{}{}{}",
|
||||||
fwdhint.unwrap_or_default(),
|
fwdhint.unwrap_or_default(),
|
||||||
&final_text,
|
escape_message_footer_marks(final_text),
|
||||||
if !final_text.is_empty() && !footer.is_empty() {
|
if !final_text.is_empty() && !footer.is_empty() {
|
||||||
"\r\n\r\n"
|
"\r\n\r\n"
|
||||||
} else {
|
} else {
|
||||||
@@ -1156,7 +1141,7 @@ async fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
|
|||||||
fn render_rfc724_mid(rfc724_mid: &str) -> String {
|
fn render_rfc724_mid(rfc724_mid: &str) -> String {
|
||||||
let rfc724_mid = rfc724_mid.trim().to_string();
|
let rfc724_mid = rfc724_mid.trim().to_string();
|
||||||
|
|
||||||
if rfc724_mid.chars().nth(0).unwrap_or_default() == '<' {
|
if rfc724_mid.chars().next().unwrap_or_default() == '<' {
|
||||||
rfc724_mid
|
rfc724_mid
|
||||||
} else {
|
} else {
|
||||||
format!("<{}>", rfc724_mid)
|
format!("<{}>", rfc724_mid)
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ use std::pin::Pin;
|
|||||||
|
|
||||||
use deltachat_derive::{FromSql, ToSql};
|
use deltachat_derive::{FromSql, ToSql};
|
||||||
use lettre_email::mime::{self, Mime};
|
use lettre_email::mime::{self, Mime};
|
||||||
use mailparse::{DispositionType, MailAddr, MailHeaderMap};
|
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
||||||
|
|
||||||
use crate::aheader::Aheader;
|
use crate::aheader::Aheader;
|
||||||
use crate::bail;
|
|
||||||
use crate::blob::BlobObject;
|
use crate::blob::BlobObject;
|
||||||
use crate::constants::Viewtype;
|
use crate::constants::Viewtype;
|
||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
@@ -15,7 +14,7 @@ use crate::context::Context;
|
|||||||
use crate::dc_tools::*;
|
use crate::dc_tools::*;
|
||||||
use crate::dehtml::dehtml;
|
use crate::dehtml::dehtml;
|
||||||
use crate::e2ee;
|
use crate::e2ee;
|
||||||
use crate::error::Result;
|
use crate::error::{bail, Result};
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||||
use crate::location;
|
use crate::location;
|
||||||
@@ -39,6 +38,11 @@ use crate::stock::StockMessage;
|
|||||||
pub struct MimeMessage {
|
pub struct MimeMessage {
|
||||||
pub parts: Vec<Part>,
|
pub parts: Vec<Part>,
|
||||||
header: HashMap<String, String>,
|
header: HashMap<String, String>,
|
||||||
|
|
||||||
|
/// Addresses are normalized and lowercased:
|
||||||
|
pub recipients: Vec<SingleInfo>,
|
||||||
|
pub from: Vec<SingleInfo>,
|
||||||
|
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||||
pub decrypting_failed: bool,
|
pub decrypting_failed: bool,
|
||||||
pub signatures: HashSet<String>,
|
pub signatures: HashSet<String>,
|
||||||
pub gossipped_addr: HashSet<String>,
|
pub gossipped_addr: HashSet<String>,
|
||||||
@@ -90,9 +94,22 @@ impl MimeMessage {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut headers = Default::default();
|
let mut headers = Default::default();
|
||||||
|
let mut recipients = Default::default();
|
||||||
|
let mut from = Default::default();
|
||||||
|
let mut chat_disposition_notification_to = None;
|
||||||
|
|
||||||
// init known headers with what mailparse provided us
|
// init known headers with what mailparse provided us
|
||||||
MimeMessage::merge_headers(&mut headers, &mail.headers);
|
MimeMessage::merge_headers(
|
||||||
|
context,
|
||||||
|
&mut headers,
|
||||||
|
&mut recipients,
|
||||||
|
&mut from,
|
||||||
|
&mut chat_disposition_notification_to,
|
||||||
|
&mail.headers,
|
||||||
|
);
|
||||||
|
|
||||||
|
// remove headers that are allowed _only_ in the encrypted part
|
||||||
|
headers.remove("secure-join-fingerprint");
|
||||||
|
|
||||||
// Memory location for a possible decrypted message.
|
// Memory location for a possible decrypted message.
|
||||||
let mail_raw;
|
let mail_raw;
|
||||||
@@ -118,7 +135,14 @@ impl MimeMessage {
|
|||||||
|
|
||||||
// let known protected headers from the decrypted
|
// let known protected headers from the decrypted
|
||||||
// part override the unencrypted top-level
|
// part override the unencrypted top-level
|
||||||
MimeMessage::merge_headers(&mut headers, &decrypted_mail.headers);
|
MimeMessage::merge_headers(
|
||||||
|
context,
|
||||||
|
&mut headers,
|
||||||
|
&mut recipients,
|
||||||
|
&mut from,
|
||||||
|
&mut chat_disposition_notification_to,
|
||||||
|
&decrypted_mail.headers,
|
||||||
|
);
|
||||||
|
|
||||||
(decrypted_mail, signatures)
|
(decrypted_mail, signatures)
|
||||||
} else {
|
} else {
|
||||||
@@ -142,6 +166,9 @@ impl MimeMessage {
|
|||||||
let mut parser = MimeMessage {
|
let mut parser = MimeMessage {
|
||||||
parts: Vec::new(),
|
parts: Vec::new(),
|
||||||
header: headers,
|
header: headers,
|
||||||
|
recipients,
|
||||||
|
from,
|
||||||
|
chat_disposition_notification_to,
|
||||||
decrypting_failed: false,
|
decrypting_failed: false,
|
||||||
|
|
||||||
// only non-empty if it was a valid autocrypt message
|
// only non-empty if it was a valid autocrypt message
|
||||||
@@ -203,10 +230,8 @@ impl MimeMessage {
|
|||||||
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
|
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
|
||||||
/// containing an explanation. If such a message is detected, first part can be safely dropped.
|
/// containing an explanation. If such a message is detected, first part can be safely dropped.
|
||||||
fn squash_attachment_parts(&mut self) {
|
fn squash_attachment_parts(&mut self) {
|
||||||
if self.has_chat_version() && self.parts.len() == 2 {
|
if let [textpart, filepart] = &self.parts[..] {
|
||||||
let need_drop = {
|
let need_drop = {
|
||||||
let textpart = &self.parts[0];
|
|
||||||
let filepart = &self.parts[1];
|
|
||||||
textpart.typ == Viewtype::Text
|
textpart.typ == Viewtype::Text
|
||||||
&& (filepart.typ == Viewtype::Image
|
&& (filepart.typ == Viewtype::Image
|
||||||
|| filepart.typ == Viewtype::Gif
|
|| filepart.typ == Viewtype::Gif
|
||||||
@@ -312,11 +337,9 @@ impl MimeMessage {
|
|||||||
|
|
||||||
// See if an MDN is requested from the other side
|
// See if an MDN is requested from the other side
|
||||||
if !self.decrypting_failed && !self.parts.is_empty() {
|
if !self.decrypting_failed && !self.parts.is_empty() {
|
||||||
if let Some(ref dn_to_addr) =
|
if let Some(ref dn_to) = self.chat_disposition_notification_to {
|
||||||
self.parse_first_addr(context, HeaderDef::ChatDispositionNotificationTo)
|
if let Some(ref from) = self.from.get(0) {
|
||||||
{
|
if from.addr == dn_to.addr {
|
||||||
if let Some(ref from_addr) = self.parse_first_addr(context, HeaderDef::From_) {
|
|
||||||
if compare_addrs(from_addr, dn_to_addr) {
|
|
||||||
if let Some(part) = self.parts.last_mut() {
|
if let Some(part) = self.parts.last_mut() {
|
||||||
part.param.set_int(Param::WantsMdn, 1);
|
part.param.set_int(Param::WantsMdn, 1);
|
||||||
}
|
}
|
||||||
@@ -390,20 +413,6 @@ impl MimeMessage {
|
|||||||
self.header.get(headerdef.get_headername())
|
self.header.get(headerdef.get_headername())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_first_addr(&self, context: &Context, headerdef: HeaderDef) -> Option<MailAddr> {
|
|
||||||
if let Some(value) = self.get(headerdef.clone()) {
|
|
||||||
match mailparse::addrparse(&value) {
|
|
||||||
Ok(ref addrs) => {
|
|
||||||
return addrs.first().cloned();
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!(context, "header {} parse error: {:?}", headerdef, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_mime_recursive<'a>(
|
fn parse_mime_recursive<'a>(
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
context: &'a Context,
|
context: &'a Context,
|
||||||
@@ -416,9 +425,9 @@ impl MimeMessage {
|
|||||||
if mail.ctype.params.get("protected-headers").is_some() {
|
if mail.ctype.params.get("protected-headers").is_some() {
|
||||||
if mail.ctype.mimetype == "text/rfc822-headers" {
|
if mail.ctype.mimetype == "text/rfc822-headers" {
|
||||||
warn!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
"Protected headers found in text/rfc822-headers attachment: Will be ignored.",
|
"Protected headers found in text/rfc822-headers attachment: Will be ignored.",
|
||||||
);
|
);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,7 +489,9 @@ impl MimeMessage {
|
|||||||
apple mail: "plaintext" as an alternative to "html+PDF attachment") */
|
apple mail: "plaintext" as an alternative to "html+PDF attachment") */
|
||||||
(mime::MULTIPART, "alternative") => {
|
(mime::MULTIPART, "alternative") => {
|
||||||
for cur_data in &mail.subparts {
|
for cur_data in &mail.subparts {
|
||||||
if get_mime_type(cur_data)?.0 == "multipart/mixed" {
|
if get_mime_type(cur_data)?.0 == "multipart/mixed"
|
||||||
|
|| get_mime_type(cur_data)?.0 == "multipart/related"
|
||||||
|
{
|
||||||
any_part_added = self.parse_mime_recursive(context, cur_data).await?;
|
any_part_added = self.parse_mime_recursive(context, cur_data).await?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -504,15 +515,6 @@ impl MimeMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(mime::MULTIPART, "related") => {
|
|
||||||
/* add the "root part" - the other parts may be referenced which is
|
|
||||||
not interesting for us (eg. embedded images) we assume he "root part"
|
|
||||||
being the first one, which may not be always true ...
|
|
||||||
however, most times it seems okay. */
|
|
||||||
if let Some(first) = mail.subparts.iter().next() {
|
|
||||||
any_part_added = self.parse_mime_recursive(context, first).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(mime::MULTIPART, "encrypted") => {
|
(mime::MULTIPART, "encrypted") => {
|
||||||
// we currently do not try to decrypt non-autocrypt messages
|
// we currently do not try to decrypt non-autocrypt messages
|
||||||
// at all. If we see an encrypted part, we set
|
// at all. If we see an encrypted part, we set
|
||||||
@@ -761,17 +763,41 @@ impl MimeMessage {
|
|||||||
.and_then(|msgid| parse_message_id(msgid).ok())
|
.and_then(|msgid| parse_message_id(msgid).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_headers(headers: &mut HashMap<String, String>, fields: &[mailparse::MailHeader<'_>]) {
|
fn merge_headers(
|
||||||
|
context: &Context,
|
||||||
|
headers: &mut HashMap<String, String>,
|
||||||
|
recipients: &mut Vec<SingleInfo>,
|
||||||
|
from: &mut Vec<SingleInfo>,
|
||||||
|
chat_disposition_notification_to: &mut Option<SingleInfo>,
|
||||||
|
fields: &[mailparse::MailHeader<'_>],
|
||||||
|
) {
|
||||||
for field in fields {
|
for field in fields {
|
||||||
// lowercasing all headers is technically not correct, but makes things work better
|
// lowercasing all headers is technically not correct, but makes things work better
|
||||||
let key = field.get_key().to_lowercase();
|
let key = field.get_key().to_lowercase();
|
||||||
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
|
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
|
||||||
is_known(&key) || key.starts_with("chat-")
|
is_known(&key) || key.starts_with("chat-")
|
||||||
{
|
{
|
||||||
let value = field.get_value();
|
if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
|
||||||
headers.insert(key.to_string(), value);
|
match addrparse_header(field) {
|
||||||
|
Ok(addrlist) => {
|
||||||
|
*chat_disposition_notification_to = addrlist.extract_single_info();
|
||||||
|
}
|
||||||
|
Err(e) => warn!(context, "Could not read {} address: {}", key, e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let value = field.get_value();
|
||||||
|
headers.insert(key.to_string(), value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let recipients_new = get_recipients(fields);
|
||||||
|
if !recipients_new.is_empty() {
|
||||||
|
*recipients = recipients_new;
|
||||||
|
}
|
||||||
|
let from_new = get_from(fields);
|
||||||
|
if !from_new.is_empty() {
|
||||||
|
*from = from_new;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_report(
|
fn process_report(
|
||||||
@@ -840,23 +866,15 @@ async fn update_gossip_peerstates(
|
|||||||
gossip_headers: Vec<String>,
|
gossip_headers: Vec<String>,
|
||||||
) -> Result<HashSet<String>> {
|
) -> Result<HashSet<String>> {
|
||||||
// XXX split the parsing from the modification part
|
// XXX split the parsing from the modification part
|
||||||
let mut recipients: Option<HashSet<String>> = None;
|
|
||||||
let mut gossipped_addr: HashSet<String> = Default::default();
|
let mut gossipped_addr: HashSet<String> = Default::default();
|
||||||
|
|
||||||
for value in &gossip_headers {
|
for value in &gossip_headers {
|
||||||
let gossip_header = value.parse::<Aheader>();
|
let gossip_header = value.parse::<Aheader>();
|
||||||
|
|
||||||
if let Ok(ref header) = gossip_header {
|
if let Ok(ref header) = gossip_header {
|
||||||
if recipients.is_none() {
|
if get_recipients(&mail.headers)
|
||||||
recipients = Some(get_recipients(
|
.iter()
|
||||||
mail.headers.iter().map(|v| (v.get_key(), v.get_value())),
|
.any(|info| info.addr == header.addr.to_lowercase())
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if recipients
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.contains(&header.addr.to_lowercase())
|
|
||||||
{
|
{
|
||||||
let mut peerstate = Peerstate::from_addr(context, &header.addr).await;
|
let mut peerstate = Peerstate::from_addr(context, &header.addr).await;
|
||||||
if let Some(ref mut peerstate) = peerstate {
|
if let Some(ref mut peerstate) = peerstate {
|
||||||
@@ -894,14 +912,29 @@ pub(crate) struct Report {
|
|||||||
additional_message_ids: Vec<String>,
|
additional_message_ids: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_message_id(value: &str) -> crate::error::Result<String> {
|
pub(crate) fn parse_message_ids(ids: &str) -> Result<Vec<String>> {
|
||||||
let ids = mailparse::msgidparse(value)
|
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
|
||||||
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
|
let mut msgids = Vec::new();
|
||||||
|
for id in ids.split_whitespace() {
|
||||||
|
let mut id = id.to_string();
|
||||||
|
if id.starts_with('<') {
|
||||||
|
id = id[1..].to_string();
|
||||||
|
}
|
||||||
|
if id.ends_with('>') {
|
||||||
|
id = id[..id.len() - 1].to_string();
|
||||||
|
}
|
||||||
|
if !id.is_empty() {
|
||||||
|
msgids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(msgids)
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(id) = ids.first() {
|
pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
|
||||||
|
if let Some(id) = parse_message_ids(ids)?.first() {
|
||||||
Ok(id.to_string())
|
Ok(id.to_string())
|
||||||
} else {
|
} else {
|
||||||
bail!("could not parse message_id: {}", value);
|
bail!("could not parse message_id: {}", ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1002,6 +1035,11 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<Option<String
|
|||||||
let desired_filename =
|
let desired_filename =
|
||||||
desired_filename.or_else(|| ct.params.get("name").map(|s| s.to_string()));
|
desired_filename.or_else(|| ct.params.get("name").map(|s| s.to_string()));
|
||||||
|
|
||||||
|
// MS Outlook is known to specify filename in the "name" attribute of
|
||||||
|
// Content-Type and omit Content-Disposition.
|
||||||
|
let desired_filename =
|
||||||
|
desired_filename.or_else(|| mail.ctype.params.get("name").map(|s| s.to_string()));
|
||||||
|
|
||||||
// If there is no filename, but part is an attachment, guess filename
|
// If there is no filename, but part is an attachment, guess filename
|
||||||
if ct.disposition == DispositionType::Attachment && desired_filename.is_none() {
|
if ct.disposition == DispositionType::Attachment && desired_filename.is_none() {
|
||||||
if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
|
if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
|
||||||
@@ -1017,50 +1055,50 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<Option<String
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// returned addresses are normalized and lowercased.
|
/// Returned addresses are normalized and lowercased.
|
||||||
fn get_recipients<S: AsRef<str>, T: Iterator<Item = (S, S)>>(headers: T) -> HashSet<String> {
|
fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||||
let mut recipients: HashSet<String> = Default::default();
|
get_all_addresses_from_header(headers, |header_key| {
|
||||||
|
header_key == "to" || header_key == "cc"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (hkey, hvalue) in headers {
|
/// Returned addresses are normalized and lowercased.
|
||||||
let hkey = hkey.as_ref().to_lowercase();
|
pub(crate) fn get_from(headers: &[MailHeader]) -> Vec<SingleInfo> {
|
||||||
let hvalue = hvalue.as_ref();
|
get_all_addresses_from_header(headers, |header_key| header_key == "from")
|
||||||
if hkey == "to" || hkey == "cc" {
|
}
|
||||||
if let Ok(addrs) = mailparse::addrparse(hvalue) {
|
|
||||||
for addr in addrs.iter() {
|
fn get_all_addresses_from_header<F>(headers: &[MailHeader], pred: F) -> Vec<SingleInfo>
|
||||||
match addr {
|
where
|
||||||
mailparse::MailAddr::Single(ref info) => {
|
F: Fn(String) -> bool,
|
||||||
recipients.insert(addr_normalize(&info.addr).to_lowercase());
|
{
|
||||||
}
|
let mut result: Vec<SingleInfo> = Default::default();
|
||||||
mailparse::MailAddr::Group(ref infos) => {
|
|
||||||
for info in &infos.addrs {
|
headers
|
||||||
recipients.insert(addr_normalize(&info.addr).to_lowercase());
|
.iter()
|
||||||
}
|
.filter(|header| pred(header.get_key().to_lowercase()))
|
||||||
|
.filter_map(|header| mailparse::addrparse_header(header).ok())
|
||||||
|
.for_each(|addrs| {
|
||||||
|
for addr in addrs.iter() {
|
||||||
|
match addr {
|
||||||
|
mailparse::MailAddr::Single(ref info) => {
|
||||||
|
result.push(SingleInfo {
|
||||||
|
addr: addr_normalize(&info.addr).to_lowercase(),
|
||||||
|
display_name: info.display_name.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mailparse::MailAddr::Group(ref infos) => {
|
||||||
|
for info in &infos.addrs {
|
||||||
|
result.push(SingleInfo {
|
||||||
|
addr: addr_normalize(&info.addr).to_lowercase(),
|
||||||
|
display_name: info.display_name.clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
recipients
|
result
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the only addrs match, ignoring names.
|
|
||||||
fn compare_addrs(a: &mailparse::MailAddr, b: &mailparse::MailAddr) -> bool {
|
|
||||||
match a {
|
|
||||||
mailparse::MailAddr::Group(group_a) => match b {
|
|
||||||
mailparse::MailAddr::Group(group_b) => group_a
|
|
||||||
.addrs
|
|
||||||
.iter()
|
|
||||||
.zip(group_b.addrs.iter())
|
|
||||||
.all(|(a, b)| a.addr == b.addr),
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
mailparse::MailAddr::Single(single_a) => match b {
|
|
||||||
mailparse::MailAddr::Single(single_b) => single_a.addr == single_b.addr,
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1113,16 +1151,13 @@ mod tests {
|
|||||||
assert_eq!(mimeparser.get_rfc724_mid(), None);
|
assert_eq!(mimeparser.get_rfc724_mid(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_std::test]
|
#[test]
|
||||||
async fn test_get_recipients() {
|
fn test_get_recipients() {
|
||||||
let context = dummy_context().await;
|
|
||||||
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
|
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
|
||||||
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
let mail = mailparse::parse_mail(&raw[..]).unwrap();
|
||||||
.await
|
let recipients = get_recipients(&mail.headers);
|
||||||
.unwrap();
|
assert!(recipients.iter().any(|info| info.addr == "abc@bcd.com"));
|
||||||
let recipients = get_recipients(mimeparser.header.iter());
|
assert!(recipients.iter().any(|info| info.addr == "def@def.de"));
|
||||||
assert!(recipients.contains("abc@bcd.com"));
|
|
||||||
assert!(recipients.contains("def@def.de"));
|
|
||||||
assert_eq!(recipients.len(), 2);
|
assert_eq!(recipients.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1179,14 +1214,10 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let of = mimeparser
|
let of = &mimeparser.from[0];
|
||||||
.parse_first_addr(&context.ctx, HeaderDef::From_)
|
assert_eq!(of.addr, "hello@one.org");
|
||||||
.unwrap();
|
|
||||||
assert_eq!(of, mailparse::addrparse("hello@one.org").unwrap()[0]);
|
|
||||||
|
|
||||||
let of =
|
assert!(mimeparser.chat_disposition_notification_to.is_none());
|
||||||
mimeparser.parse_first_addr(&context.ctx, HeaderDef::ChatDispositionNotificationTo);
|
|
||||||
assert!(of.is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_std::test]
|
#[async_std::test]
|
||||||
@@ -1196,14 +1227,16 @@ mod tests {
|
|||||||
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
|
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
|
||||||
Subject: outer-subject\n\
|
Subject: outer-subject\n\
|
||||||
Secure-Join-Group: no\n\
|
Secure-Join-Group: no\n\
|
||||||
Test-Header: Bar\nChat-Version: 0.0\n\
|
Secure-Join-Fingerprint: 123456\n\
|
||||||
|
Test-Header: Bar\n\
|
||||||
|
chat-VERSION: 0.0\n\
|
||||||
\n\
|
\n\
|
||||||
--==break==\n\
|
--==break==\n\
|
||||||
Content-Type: text/plain; protected-headers=\"v1\";\n\
|
Content-Type: text/plain; protected-headers=\"v1\";\n\
|
||||||
Subject: inner-subject\n\
|
Subject: inner-subject\n\
|
||||||
SecureBar-Join-Group: yes\n\
|
SecureBar-Join-Group: yes\n\
|
||||||
Test-Header: Xy\n\
|
Test-Header: Xy\n\
|
||||||
Chat-Version: 1.0\n\
|
chat-VERSION: 1.0\n\
|
||||||
\n\
|
\n\
|
||||||
test1\n\
|
test1\n\
|
||||||
\n\
|
\n\
|
||||||
@@ -1224,12 +1257,17 @@ mod tests {
|
|||||||
|
|
||||||
// the following fields would bubble up
|
// the following fields would bubble up
|
||||||
// if the test would really use encryption for the protected part
|
// if the test would really use encryption for the protected part
|
||||||
// however, as this is not the case, the outer things stay valid
|
// however, as this is not the case, the outer things stay valid.
|
||||||
|
// for Chat-Version, also the case-insensivity is tested.
|
||||||
assert_eq!(mimeparser.get_subject(), Some("outer-subject".into()));
|
assert_eq!(mimeparser.get_subject(), Some("outer-subject".into()));
|
||||||
|
|
||||||
let of = mimeparser.get(HeaderDef::ChatVersion).unwrap();
|
let of = mimeparser.get(HeaderDef::ChatVersion).unwrap();
|
||||||
assert_eq!(of, "0.0");
|
assert_eq!(of, "0.0");
|
||||||
assert_eq!(mimeparser.parts.len(), 1);
|
assert_eq!(mimeparser.parts.len(), 1);
|
||||||
|
|
||||||
|
// make sure, headers that are only allowed in the encrypted part
|
||||||
|
// cannot be set from the outer part
|
||||||
|
assert!(mimeparser.get(HeaderDef::SecureJoinFingerprint).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_std::test]
|
#[async_std::test]
|
||||||
@@ -1533,6 +1571,266 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
|
|||||||
Some("Mail with inline attachment".to_string())
|
Some("Mail with inline attachment".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(message.parts.len(), 2);
|
assert_eq!(message.parts.len(), 1);
|
||||||
|
assert_eq!(message.parts[0].typ, Viewtype::File);
|
||||||
|
assert_eq!(message.parts[0].msg, "Hello!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn parse_inline_image() {
|
||||||
|
let context = dummy_context().await;
|
||||||
|
let raw = br#"Message-ID: <foobar@example.org>
|
||||||
|
From: foo <foo@example.org>
|
||||||
|
Subject: example
|
||||||
|
To: bar@example.org
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: multipart/mixed; boundary="--11019878869865180"
|
||||||
|
|
||||||
|
----11019878869865180
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
|
||||||
|
Test
|
||||||
|
|
||||||
|
----11019878869865180
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="JPEG_filename.jpg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename="JPEG_filename.jpg"
|
||||||
|
|
||||||
|
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
|
||||||
|
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
|
||||||
|
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
|
||||||
|
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
|
||||||
|
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
|
||||||
|
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
|
||||||
|
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
|
||||||
|
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
|
||||||
|
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||||
|
|
||||||
|
|
||||||
|
----11019878869865180--
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(message.get_subject(), Some("example".to_string()));
|
||||||
|
|
||||||
|
assert_eq!(message.parts.len(), 1);
|
||||||
|
assert_eq!(message.parts[0].typ, Viewtype::Image);
|
||||||
|
assert_eq!(message.parts[0].msg, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn parse_thunderbird_html_embedded_image() {
|
||||||
|
let context = dummy_context().await;
|
||||||
|
let raw = br#"To: Alice <alice@example.org>
|
||||||
|
From: Bob <bob@example.org>
|
||||||
|
Subject: Test subject
|
||||||
|
Message-ID: <foobarbaz@example.org>
|
||||||
|
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101
|
||||||
|
Thunderbird/68.7.0
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="------------779C1631600DF3DB8C02E53A"
|
||||||
|
Content-Language: en-US
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
--------------779C1631600DF3DB8C02E53A
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Test
|
||||||
|
|
||||||
|
|
||||||
|
--------------779C1631600DF3DB8C02E53A
|
||||||
|
Content-Type: multipart/related;
|
||||||
|
boundary="------------10CC6C2609EB38DA782C5CA9"
|
||||||
|
|
||||||
|
|
||||||
|
--------------10CC6C2609EB38DA782C5CA9
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Test<br>
|
||||||
|
<p><img moz-do-not-send="false" src="cid:part1.9DFA679B.52A88D69@example.org" alt=""></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
--------------10CC6C2609EB38DA782C5CA9
|
||||||
|
Content-Type: image/png;
|
||||||
|
name="1.png"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-ID: <part1.9DFA679B.52A88D69@example.org>
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename="1.png"
|
||||||
|
|
||||||
|
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
|
||||||
|
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
|
||||||
|
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
|
||||||
|
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
|
||||||
|
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
|
||||||
|
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
|
||||||
|
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
|
||||||
|
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
|
||||||
|
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||||
|
--------------10CC6C2609EB38DA782C5CA9--
|
||||||
|
|
||||||
|
--------------779C1631600DF3DB8C02E53A--"#;
|
||||||
|
|
||||||
|
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(message.get_subject(), Some("Test subject".to_string()));
|
||||||
|
|
||||||
|
assert_eq!(message.parts.len(), 1);
|
||||||
|
assert_eq!(message.parts[0].typ, Viewtype::Image);
|
||||||
|
assert_eq!(message.parts[0].msg, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outlook specifies filename in the "name" attribute of Content-Type
|
||||||
|
#[async_std::test]
|
||||||
|
async fn parse_outlook_html_embedded_image() {
|
||||||
|
let context = dummy_context().await;
|
||||||
|
let raw = br##"From: Anonymous <anonymous@example.org>
|
||||||
|
To: Anonymous <anonymous@example.org>
|
||||||
|
Subject: Delta Chat is great stuff!
|
||||||
|
Date: Tue, 5 May 2020 01:23:45 +0000
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: multipart/related;
|
||||||
|
boundary="----=_NextPart_000_0003_01D622B3.CA753E60"
|
||||||
|
X-Mailer: Microsoft Outlook 15.0
|
||||||
|
|
||||||
|
This is a multipart message in MIME format.
|
||||||
|
|
||||||
|
------=_NextPart_000_0003_01D622B3.CA753E60
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="----=_NextPart_001_0004_01D622B3.CA753E60"
|
||||||
|
|
||||||
|
|
||||||
|
------=_NextPart_001_0004_01D622B3.CA753E60
|
||||||
|
Content-Type: text/plain;
|
||||||
|
charset="us-ascii"
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
------=_NextPart_001_0004_01D622B3.CA753E60
|
||||||
|
Content-Type: text/html;
|
||||||
|
charset="us-ascii"
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
Test<img src="cid:image001.jpg@01D622B3.C9D8D750">
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
------=_NextPart_001_0004_01D622B3.CA753E60--
|
||||||
|
|
||||||
|
------=_NextPart_000_0003_01D622B3.CA753E60
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="image001.jpg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-ID: <image001.jpg@01D622B3.C9D8D750>
|
||||||
|
|
||||||
|
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
|
||||||
|
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
|
||||||
|
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
|
||||||
|
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
|
||||||
|
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
|
||||||
|
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
|
||||||
|
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
|
||||||
|
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
|
||||||
|
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||||
|
|
||||||
|
------=_NextPart_000_0003_01D622B3.CA753E60--
|
||||||
|
"##;
|
||||||
|
|
||||||
|
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
message.get_subject(),
|
||||||
|
Some("Delta Chat is great stuff!".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(message.parts.len(), 1);
|
||||||
|
assert_eq!(message.parts[0].typ, Viewtype::Image);
|
||||||
|
assert_eq!(message.parts[0].msg, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_message_id() {
|
||||||
|
let test = parse_message_id("<foobar>");
|
||||||
|
assert!(test.is_ok());
|
||||||
|
assert_eq!(test.unwrap(), "foobar");
|
||||||
|
|
||||||
|
let test = parse_message_id("<foo> <bar>");
|
||||||
|
assert!(test.is_ok());
|
||||||
|
assert_eq!(test.unwrap(), "foo");
|
||||||
|
|
||||||
|
let test = parse_message_id(" < foo > <bar>");
|
||||||
|
assert!(test.is_ok());
|
||||||
|
assert_eq!(test.unwrap(), "foo");
|
||||||
|
|
||||||
|
let test = parse_message_id("foo");
|
||||||
|
assert!(test.is_ok());
|
||||||
|
assert_eq!(test.unwrap(), "foo");
|
||||||
|
|
||||||
|
let test = parse_message_id(" foo ");
|
||||||
|
assert!(test.is_ok());
|
||||||
|
assert_eq!(test.unwrap(), "foo");
|
||||||
|
|
||||||
|
let test = parse_message_id("foo bar");
|
||||||
|
assert!(test.is_ok());
|
||||||
|
assert_eq!(test.unwrap(), "foo");
|
||||||
|
|
||||||
|
let test = parse_message_id(" foo bar ");
|
||||||
|
assert!(test.is_ok());
|
||||||
|
assert_eq!(test.unwrap(), "foo");
|
||||||
|
|
||||||
|
let test = parse_message_id("");
|
||||||
|
assert!(test.is_err());
|
||||||
|
|
||||||
|
let test = parse_message_id(" ");
|
||||||
|
assert!(test.is_err());
|
||||||
|
|
||||||
|
let test = parse_message_id("<>");
|
||||||
|
assert!(test.is_err());
|
||||||
|
|
||||||
|
let test = parse_message_id("<> bar");
|
||||||
|
assert!(test.is_ok());
|
||||||
|
assert_eq!(test.unwrap(), "bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_message_ids() {
|
||||||
|
let test = parse_message_ids(" foo bar <foobar>").unwrap();
|
||||||
|
assert_eq!(test.len(), 3);
|
||||||
|
assert_eq!(test[0], "foo");
|
||||||
|
assert_eq!(test[1], "bar");
|
||||||
|
assert_eq!(test[2], "foobar");
|
||||||
|
|
||||||
|
let test = parse_message_ids(" < foobar >").unwrap();
|
||||||
|
assert_eq!(test.len(), 1);
|
||||||
|
assert_eq!(test[0], "foobar");
|
||||||
|
|
||||||
|
let test = parse_message_ids("").unwrap();
|
||||||
|
assert!(test.is_empty());
|
||||||
|
|
||||||
|
let test = parse_message_ids(" ").unwrap();
|
||||||
|
assert!(test.is_empty());
|
||||||
|
|
||||||
|
let test = parse_message_ids(" < ").unwrap();
|
||||||
|
assert!(test.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::blob::{BlobError, BlobObject};
|
use crate::blob::{BlobError, BlobObject};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::error;
|
use crate::error::{self, bail, ensure};
|
||||||
use crate::message::MsgId;
|
use crate::message::MsgId;
|
||||||
use crate::mimeparser::SystemMessage;
|
use crate::mimeparser::SystemMessage;
|
||||||
|
|
||||||
|
|||||||
18
src/pgp.rs
18
src/pgp.rs
@@ -18,7 +18,7 @@ use rand::{thread_rng, CryptoRng, Rng};
|
|||||||
|
|
||||||
use crate::constants::KeyGenType;
|
use crate::constants::KeyGenType;
|
||||||
use crate::dc_tools::EmailAddress;
|
use crate::dc_tools::EmailAddress;
|
||||||
use crate::error::Result;
|
use crate::error::{bail, ensure, format_err, Result};
|
||||||
use crate::key::*;
|
use crate::key::*;
|
||||||
use crate::keyring::*;
|
use crate::keyring::*;
|
||||||
|
|
||||||
@@ -117,21 +117,19 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
|
|||||||
///
|
///
|
||||||
/// Most of these are likely coding errors rather than user errors
|
/// Most of these are likely coding errors rather than user errors
|
||||||
/// since all variability is hardcoded.
|
/// since all variability is hardcoded.
|
||||||
#[derive(Fail, Debug)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
#[fail(display = "PgpKeygenError: {}", message)]
|
#[error("PgpKeygenError: {message}")]
|
||||||
pub(crate) struct PgpKeygenError {
|
pub struct PgpKeygenError {
|
||||||
message: String,
|
message: String,
|
||||||
#[cause]
|
#[source]
|
||||||
cause: failure::Error,
|
cause: anyhow::Error,
|
||||||
backtrace: failure::Backtrace,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PgpKeygenError {
|
impl PgpKeygenError {
|
||||||
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
|
fn new(message: impl Into<String>, cause: impl Into<anyhow::Error>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
cause: cause.into(),
|
cause: cause.into(),
|
||||||
backtrace: failure::Backtrace::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +187,7 @@ pub(crate) fn create_keypair(
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|err| PgpKeygenError::new("invalid key params", failure::err_msg(err)))?;
|
.map_err(|err| PgpKeygenError::new("invalid key params", format_err!(err)))?;
|
||||||
let key = key_params
|
let key = key_params
|
||||||
.generate()
|
.generate()
|
||||||
.map_err(|err| PgpKeygenError::new("invalid params", err))?;
|
.map_err(|err| PgpKeygenError::new("invalid params", err))?;
|
||||||
|
|||||||
@@ -268,7 +268,14 @@ lazy_static::lazy_static! {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// yandex.ru.md: yandex.ru, yandex.com
|
// yandex.ru.md: yandex.ru, yandex.com
|
||||||
// - skipping provider with status OK and no special things to do
|
static ref P_YANDEX_RU: Provider = Provider {
|
||||||
|
status: Status::PREPARATION,
|
||||||
|
before_login_hint: "For Yandex accounts, you have to set IMAP protocol option turned on.",
|
||||||
|
after_login_hint: "",
|
||||||
|
overview_page: "https://providers.delta.chat/yandex-ru",
|
||||||
|
server: vec![
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
// ziggo.nl.md: ziggo.nl
|
// ziggo.nl.md: ziggo.nl
|
||||||
static ref P_ZIGGO_NL: Provider = Provider {
|
static ref P_ZIGGO_NL: Provider = Provider {
|
||||||
@@ -360,6 +367,8 @@ lazy_static::lazy_static! {
|
|||||||
("ymail.com", &*P_YAHOO),
|
("ymail.com", &*P_YAHOO),
|
||||||
("rocketmail.com", &*P_YAHOO),
|
("rocketmail.com", &*P_YAHOO),
|
||||||
("yahoodns.net", &*P_YAHOO),
|
("yahoodns.net", &*P_YAHOO),
|
||||||
|
("yandex.ru", &*P_YANDEX_RU),
|
||||||
|
("yandex.com", &*P_YANDEX_RU),
|
||||||
("ziggo.nl", &*P_ZIGGO_NL),
|
("ziggo.nl", &*P_ZIGGO_NL),
|
||||||
].iter().copied().collect();
|
].iter().copied().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
83
src/qr.rs
83
src/qr.rs
@@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use percent_encoding::percent_decode_str;
|
use percent_encoding::percent_decode_str;
|
||||||
|
use reqwest::Url;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::chat;
|
use crate::chat;
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::constants::Blocked;
|
use crate::constants::Blocked;
|
||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::error::Error;
|
use crate::error::{bail, ensure, format_err, Error};
|
||||||
use crate::key::dc_format_fingerprint;
|
use crate::key::dc_format_fingerprint;
|
||||||
use crate::key::dc_normalize_fingerprint;
|
use crate::key::dc_normalize_fingerprint;
|
||||||
use crate::lot::{Lot, LotState};
|
use crate::lot::{Lot, LotState};
|
||||||
use crate::param::*;
|
use crate::param::*;
|
||||||
use crate::peerstate::*;
|
use crate::peerstate::*;
|
||||||
use reqwest::Url;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
|
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
|
||||||
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
|
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
|
||||||
@@ -37,6 +37,10 @@ impl Into<Lot> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
|
||||||
|
string.to_lowercase().starts_with(&pattern.to_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
/// Check a scanned QR code.
|
/// Check a scanned QR code.
|
||||||
/// The function should be called after a QR code is scanned.
|
/// The function should be called after a QR code is scanned.
|
||||||
/// The function takes the raw text scanned and checks what can be done with it.
|
/// The function takes the raw text scanned and checks what can be done with it.
|
||||||
@@ -45,9 +49,9 @@ pub async fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
|
|||||||
|
|
||||||
info!(context, "Scanned QR code: {}", qr);
|
info!(context, "Scanned QR code: {}", qr);
|
||||||
|
|
||||||
if qr.starts_with(OPENPGP4FPR_SCHEME) {
|
if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
|
||||||
decode_openpgp(context, qr).await
|
decode_openpgp(context, qr).await
|
||||||
} else if qr.starts_with(DCACCOUNT_SCHEME) {
|
} else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) {
|
||||||
decode_account(context, qr)
|
decode_account(context, qr)
|
||||||
} else if qr.starts_with(MAILTO_SCHEME) {
|
} else if qr.starts_with(MAILTO_SCHEME) {
|
||||||
decode_mailto(context, qr).await
|
decode_mailto(context, qr).await
|
||||||
@@ -224,27 +228,20 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error
|
|||||||
|
|
||||||
let response = reqwest::blocking::Client::new().post(url_str).send();
|
let response = reqwest::blocking::Client::new().post(url_str).send();
|
||||||
if response.is_err() {
|
if response.is_err() {
|
||||||
return Err(format_err!(
|
bail!("Cannot create account, request to {} failed", url_str);
|
||||||
"Cannot create account, request to {} failed",
|
|
||||||
url_str
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
let response = response.unwrap();
|
let response = response.unwrap();
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(format_err!(
|
bail!("Request to {} unsuccessful: {:?}", url_str, response);
|
||||||
"Request to {} unsuccessful: {:?}",
|
|
||||||
url_str,
|
|
||||||
response
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed: reqwest::Result<CreateAccountResponse> = response.json();
|
let parsed: reqwest::Result<CreateAccountResponse> = response.json();
|
||||||
if parsed.is_err() {
|
if parsed.is_err() {
|
||||||
return Err(format_err!(
|
bail!(
|
||||||
"Failed to parse JSON response from {}: error: {:?}",
|
"Failed to parse JSON response from {}: error: {:?}",
|
||||||
url_str,
|
url_str,
|
||||||
parsed
|
parsed
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
println!("response: {:?}", &parsed);
|
println!("response: {:?}", &parsed);
|
||||||
let parsed = parsed.unwrap();
|
let parsed = parsed.unwrap();
|
||||||
@@ -296,7 +293,6 @@ async fn decode_smtp(context: &Context, qr: &str) -> Lot {
|
|||||||
Ok(addr) => addr,
|
Ok(addr) => addr,
|
||||||
Err(err) => return err.into(),
|
Err(err) => return err.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let name = "".to_string();
|
let name = "".to_string();
|
||||||
Lot::from_address(context, name, addr).await
|
Lot::from_address(context, name, addr).await
|
||||||
}
|
}
|
||||||
@@ -534,6 +530,17 @@ mod tests {
|
|||||||
assert_ne!(res.get_id(), 0);
|
assert_ne!(res.get_id(), 0);
|
||||||
assert_eq!(res.get_text1().unwrap(), "test ? test !");
|
assert_eq!(res.get_text1().unwrap(), "test ? test !");
|
||||||
|
|
||||||
|
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||||
|
let res = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||||
|
).await;
|
||||||
|
|
||||||
|
println!("{:?}", res);
|
||||||
|
assert_eq!(res.get_state(), LotState::QrAskVerifyGroup);
|
||||||
|
assert_ne!(res.get_id(), 0);
|
||||||
|
assert_eq!(res.get_text1().unwrap(), "test ? test !");
|
||||||
|
|
||||||
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap();
|
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap();
|
||||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||||
}
|
}
|
||||||
@@ -551,6 +558,16 @@ mod tests {
|
|||||||
assert_eq!(res.get_state(), LotState::QrAskVerifyContact);
|
assert_eq!(res.get_state(), LotState::QrAskVerifyContact);
|
||||||
assert_ne!(res.get_id(), 0);
|
assert_ne!(res.get_id(), 0);
|
||||||
|
|
||||||
|
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||||
|
let res = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||||
|
).await;
|
||||||
|
|
||||||
|
println!("{:?}", res);
|
||||||
|
assert_eq!(res.get_state(), LotState::QrAskVerifyContact);
|
||||||
|
assert_ne!(res.get_id(), 0);
|
||||||
|
|
||||||
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap();
|
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap();
|
||||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||||
assert_eq!(contact.get_name(), "Jörn P. P.");
|
assert_eq!(contact.get_name(), "Jörn P. P.");
|
||||||
@@ -572,6 +589,20 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(res.get_id(), 0);
|
assert_eq!(res.get_id(), 0);
|
||||||
|
|
||||||
|
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||||
|
|
||||||
|
let res = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"openpgp4fpr:1234567890123456789012345678901234567890",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.get_state(), LotState::QrFprWithoutAddr);
|
||||||
|
assert_eq!(
|
||||||
|
res.get_text1().unwrap(),
|
||||||
|
"1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890"
|
||||||
|
);
|
||||||
|
assert_eq!(res.get_id(), 0);
|
||||||
|
|
||||||
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await;
|
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await;
|
||||||
assert_eq!(res.get_state(), LotState::QrError);
|
assert_eq!(res.get_state(), LotState::QrError);
|
||||||
assert_eq!(res.get_id(), 0);
|
assert_eq!(res.get_id(), 0);
|
||||||
@@ -588,6 +619,15 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
assert_eq!(res.get_state(), LotState::QrAccount);
|
assert_eq!(res.get_state(), LotState::QrAccount);
|
||||||
assert_eq!(res.get_text1().unwrap(), "example.org");
|
assert_eq!(res.get_text1().unwrap(), "example.org");
|
||||||
|
|
||||||
|
// Test it again with lowercased "dcaccount:" uri scheme
|
||||||
|
let res = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.get_state(), LotState::QrAccount);
|
||||||
|
assert_eq!(res.get_text1().unwrap(), "example.org");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_std::test]
|
#[async_std::test]
|
||||||
@@ -600,5 +640,14 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
assert_eq!(res.get_state(), LotState::QrError);
|
assert_eq!(res.get_state(), LotState::QrError);
|
||||||
assert!(res.get_text1().is_some());
|
assert!(res.get_text1().is_some());
|
||||||
|
|
||||||
|
// Test it again with lowercased "dcaccount:" uri scheme
|
||||||
|
let res = check_qr(
|
||||||
|
&ctx.ctx,
|
||||||
|
"dcaccount:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.get_state(), LotState::QrError);
|
||||||
|
assert!(res.get_text1().is_some());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ use crate::constants::*;
|
|||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::e2ee::*;
|
use crate::e2ee::*;
|
||||||
use crate::error::Error;
|
use crate::error::{bail, Error};
|
||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
use crate::headerdef::HeaderDef;
|
use crate::headerdef::HeaderDef;
|
||||||
use crate::key::{dc_normalize_fingerprint, Key};
|
use crate::key::{dc_normalize_fingerprint, DcKey, Key, SignedPublicKey};
|
||||||
use crate::lot::LotState;
|
use crate::lot::LotState;
|
||||||
use crate::message::Message;
|
use crate::message::Message;
|
||||||
use crate::mimeparser::*;
|
use crate::mimeparser::*;
|
||||||
@@ -140,12 +140,13 @@ pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> O
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_self_fingerprint(context: &Context) -> Option<String> {
|
async fn get_self_fingerprint(context: &Context) -> Option<String> {
|
||||||
if let Some(self_addr) = context.get_config(Config::ConfiguredAddr).await {
|
match SignedPublicKey::load_self(context).await {
|
||||||
if let Some(key) = Key::from_self_public(context, self_addr, &context.sql).await {
|
Ok(key) => Some(Key::from(key).fingerprint()),
|
||||||
return Some(key.fingerprint());
|
Err(_) => {
|
||||||
|
warn!(context, "get_self_fingerprint(): failed to load key");
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cleanup(
|
async fn cleanup(
|
||||||
@@ -374,24 +375,21 @@ async fn fingerprint_equals_sender(
|
|||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
#[derive(Fail, Debug)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub(crate) enum HandshakeError {
|
pub(crate) enum HandshakeError {
|
||||||
#[fail(display = "Can not be called with special contact ID")]
|
#[error("Can not be called with special contact ID")]
|
||||||
SpecialContactId,
|
SpecialContactId,
|
||||||
#[fail(display = "Not a Secure-Join message")]
|
#[error("Not a Secure-Join message")]
|
||||||
NotSecureJoinMsg,
|
NotSecureJoinMsg,
|
||||||
#[fail(
|
#[error("Failed to look up or create chat for contact #{contact_id}")]
|
||||||
display = "Failed to look up or create chat for contact #{}",
|
|
||||||
contact_id
|
|
||||||
)]
|
|
||||||
NoChat {
|
NoChat {
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
#[cause]
|
#[source]
|
||||||
cause: Error,
|
cause: Error,
|
||||||
},
|
},
|
||||||
#[fail(display = "Chat for group {} not found", group)]
|
#[error("Chat for group {group} not found")]
|
||||||
ChatNotFound { group: String },
|
ChatNotFound { group: String },
|
||||||
#[fail(display = "No configured self address found")]
|
#[error("No configured self address found")]
|
||||||
NoSelfAddr,
|
NoSelfAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,8 +422,6 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
mime_message: &MimeMessage,
|
mime_message: &MimeMessage,
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
) -> Result<HandshakeMessage, HandshakeError> {
|
) -> Result<HandshakeMessage, HandshakeError> {
|
||||||
let own_fingerprint: String;
|
|
||||||
|
|
||||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||||
return Err(HandshakeError::SpecialContactId);
|
return Err(HandshakeError::SpecialContactId);
|
||||||
}
|
}
|
||||||
@@ -546,7 +542,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
return Ok(HandshakeMessage::Ignore);
|
return Ok(HandshakeMessage::Ignore);
|
||||||
}
|
}
|
||||||
info!(context, "Fingerprint verified.",);
|
info!(context, "Fingerprint verified.",);
|
||||||
own_fingerprint = get_self_fingerprint(context).await.unwrap();
|
let own_fingerprint = get_self_fingerprint(context).await.unwrap();
|
||||||
joiner_progress!(context, contact_id, 400);
|
joiner_progress!(context, contact_id, 400);
|
||||||
context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM;
|
context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM;
|
||||||
|
|
||||||
@@ -666,11 +662,18 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Alice -> Bob
|
// Alice -> Bob
|
||||||
send_handshake_msg(context, contact_chat_id, "vc-contact-confirm", "", None, "")
|
send_handshake_msg(
|
||||||
.await;
|
context,
|
||||||
|
contact_chat_id,
|
||||||
|
"vc-contact-confirm",
|
||||||
|
"",
|
||||||
|
Some(fingerprint.clone()),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
inviter_progress!(context, contact_id, 1000);
|
inviter_progress!(context, contact_id, 1000);
|
||||||
}
|
}
|
||||||
Ok(HandshakeMessage::Done)
|
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||||
}
|
}
|
||||||
"vg-member-added" | "vc-contact-confirm" => {
|
"vg-member-added" | "vc-contact-confirm" => {
|
||||||
/*=======================================================
|
/*=======================================================
|
||||||
@@ -762,27 +765,31 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
}
|
}
|
||||||
secure_connection_established(context, contact_chat_id).await;
|
secure_connection_established(context, contact_chat_id).await;
|
||||||
context.bob.write().await.expects = 0;
|
context.bob.write().await.expects = 0;
|
||||||
if join_vg {
|
|
||||||
// Bob -> Alice
|
// Bob -> Alice
|
||||||
send_handshake_msg(
|
send_handshake_msg(
|
||||||
context,
|
context,
|
||||||
contact_chat_id,
|
contact_chat_id,
|
||||||
"vg-member-added-received",
|
if join_vg {
|
||||||
"",
|
"vg-member-added-received"
|
||||||
None,
|
} else {
|
||||||
"",
|
"vc-contact-confirm-received" // only for observe_securejoin_on_other_device()
|
||||||
)
|
},
|
||||||
.await;
|
"",
|
||||||
}
|
Some(scanned_fingerprint_of_alice),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
context.bob.write().await.status = 1;
|
context.bob.write().await.status = 1;
|
||||||
context.stop_ongoing().await;
|
context.stop_ongoing().await;
|
||||||
Ok(if join_vg {
|
Ok(if join_vg {
|
||||||
HandshakeMessage::Propagate
|
HandshakeMessage::Propagate
|
||||||
} else {
|
} else {
|
||||||
HandshakeMessage::Done
|
HandshakeMessage::Ignore // "Done" deletes the message and breaks multi-device
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"vg-member-added-received" => {
|
"vg-member-added-received" | "vc-contact-confirm-received" => {
|
||||||
/*==========================================================
|
/*==========================================================
|
||||||
==== Alice - the inviter side ====
|
==== Alice - the inviter side ====
|
||||||
==== Step 8 in "Out-of-band verified groups" protocol ====
|
==== Step 8 in "Out-of-band verified groups" protocol ====
|
||||||
@@ -790,30 +797,26 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
|
|
||||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||||
if contact.is_verified(context).await == VerifiedStatus::Unverified {
|
if contact.is_verified(context).await == VerifiedStatus::Unverified {
|
||||||
warn!(context, "vg-member-added-received invalid.",);
|
warn!(context, "{} invalid.", step);
|
||||||
return Ok(HandshakeMessage::Ignore);
|
return Ok(HandshakeMessage::Ignore);
|
||||||
}
|
}
|
||||||
inviter_progress!(context, contact_id, 800);
|
if join_vg {
|
||||||
inviter_progress!(context, contact_id, 1000);
|
inviter_progress!(context, contact_id, 800);
|
||||||
let field_grpid = mime_message
|
inviter_progress!(context, contact_id, 1000);
|
||||||
.get(HeaderDef::SecureJoinGroup)
|
let field_grpid = mime_message
|
||||||
.map(|s| s.as_str())
|
.get(HeaderDef::SecureJoinGroup)
|
||||||
.unwrap_or_else(|| "");
|
.map(|s| s.as_str())
|
||||||
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid)
|
.unwrap_or_else(|| "");
|
||||||
.await
|
if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid).await {
|
||||||
.map_err(|err| {
|
|
||||||
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
|
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
|
||||||
HandshakeError::ChatNotFound {
|
return Err(HandshakeError::ChatNotFound {
|
||||||
group: field_grpid.to_string(),
|
group: field_grpid.to_string(),
|
||||||
}
|
});
|
||||||
})?;
|
}
|
||||||
context.call_cb(Event::SecurejoinMemberAdded {
|
}
|
||||||
chat_id: group_chat_id,
|
Ok(HandshakeMessage::Ignore) // "Done" deletes the message and breaks multi-device
|
||||||
contact_id,
|
|
||||||
});
|
|
||||||
Ok(HandshakeMessage::Done)
|
|
||||||
} else {
|
} else {
|
||||||
warn!(context, "vg-member-added-received invalid.",);
|
warn!(context, "{} invalid.", step);
|
||||||
Ok(HandshakeMessage::Ignore)
|
Ok(HandshakeMessage::Ignore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -825,8 +828,6 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// observe_securejoin_on_other_device() must be called when a self-sent securejoin message is seen.
|
/// observe_securejoin_on_other_device() must be called when a self-sent securejoin message is seen.
|
||||||
/// currently, the message is only ignored, in the future,
|
|
||||||
/// we may mark peers as verified accross devices:
|
|
||||||
///
|
///
|
||||||
/// in a multi-device-setup, there may be other devices that "see" the handshake messages.
|
/// in a multi-device-setup, there may be other devices that "see" the handshake messages.
|
||||||
/// if the seen messages seen are self-sent messages encrypted+signed correctly with our key,
|
/// if the seen messages seen are self-sent messages encrypted+signed correctly with our key,
|
||||||
@@ -843,17 +844,82 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
/// the joining device has marked the peer as verified on vg-member-added/vc-contact-confirm
|
/// the joining device has marked the peer as verified on vg-member-added/vc-contact-confirm
|
||||||
/// before sending vg-member-added-received - so, if we observe vg-member-added-received,
|
/// before sending vg-member-added-received - so, if we observe vg-member-added-received,
|
||||||
/// we can mark the peer as verified as well.
|
/// we can mark the peer as verified as well.
|
||||||
///
|
pub(crate) async fn observe_securejoin_on_other_device(
|
||||||
/// to make this work, (a) some messages must not be deleted,
|
context: &Context,
|
||||||
/// (b) we need a vc-contact-confirm-received message if bcc_self is set,
|
mime_message: &MimeMessage,
|
||||||
/// (c) we should make sure, we do not only rely on the unencrypted To:-header for identifying the peer
|
contact_id: u32,
|
||||||
/// (in handle_securejoin_handshake() we have the oob information for that)
|
|
||||||
pub(crate) fn observe_securejoin_on_other_device(
|
|
||||||
_context: &Context,
|
|
||||||
_mime_message: &MimeMessage,
|
|
||||||
_contact_id: u32,
|
|
||||||
) -> Result<HandshakeMessage, HandshakeError> {
|
) -> Result<HandshakeMessage, HandshakeError> {
|
||||||
Ok(HandshakeMessage::Ignore)
|
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||||
|
return Err(HandshakeError::SpecialContactId);
|
||||||
|
}
|
||||||
|
let step = mime_message
|
||||||
|
.get(HeaderDef::SecureJoin)
|
||||||
|
.ok_or(HandshakeError::NotSecureJoinMsg)?;
|
||||||
|
info!(context, "observing secure-join message \'{}\'", step);
|
||||||
|
|
||||||
|
let contact_chat_id =
|
||||||
|
match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not).await {
|
||||||
|
Ok((chat_id, blocked)) => {
|
||||||
|
if blocked != Blocked::Not {
|
||||||
|
chat_id.unblock(context).await;
|
||||||
|
}
|
||||||
|
chat_id
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(HandshakeError::NoChat {
|
||||||
|
contact_id,
|
||||||
|
cause: err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match step.as_str() {
|
||||||
|
"vg-member-added"
|
||||||
|
| "vc-contact-confirm"
|
||||||
|
| "vg-member-added-received"
|
||||||
|
| "vc-contact-confirm-received" => {
|
||||||
|
if !encrypted_and_signed(
|
||||||
|
context,
|
||||||
|
mime_message,
|
||||||
|
get_self_fingerprint(context).await.unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
could_not_establish_secure_connection(
|
||||||
|
context,
|
||||||
|
contact_chat_id,
|
||||||
|
"Message not encrypted correctly.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Ok(HandshakeMessage::Ignore);
|
||||||
|
}
|
||||||
|
let fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint) {
|
||||||
|
Some(fp) => fp,
|
||||||
|
None => {
|
||||||
|
could_not_establish_secure_connection(
|
||||||
|
context,
|
||||||
|
contact_chat_id,
|
||||||
|
"Fingerprint not provided, please update Delta Chat on all your devices.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Ok(HandshakeMessage::Ignore);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if mark_peer_as_verified(context, fingerprint).await.is_err() {
|
||||||
|
could_not_establish_secure_connection(
|
||||||
|
context,
|
||||||
|
contact_chat_id,
|
||||||
|
format!("Fingerprint mismatch on observing {}.", step).as_ref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Ok(HandshakeMessage::Ignore);
|
||||||
|
}
|
||||||
|
Ok(if step.as_str() == "vg-member-added" {
|
||||||
|
HandshakeMessage::Propagate
|
||||||
|
} else {
|
||||||
|
HandshakeMessage::Ignore
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Ok(HandshakeMessage::Ignore),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
|
async fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
|
||||||
|
|||||||
149
src/simplify.rs
149
src/simplify.rs
@@ -1,13 +1,41 @@
|
|||||||
|
// protect lines starting with `--` against being treated as a footer.
|
||||||
|
// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
|
||||||
|
// this should be invisible on most systems and there is no need to unescape it again
|
||||||
|
// (which won't be done by non-deltas anyway)
|
||||||
|
//
|
||||||
|
// this escapes a bit more than actually needed by delta (eg. also lines as "-- footer"),
|
||||||
|
// but for non-delta-compatibility, that seems to be better.
|
||||||
|
// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
|
||||||
|
pub fn escape_message_footer_marks(text: &str) -> String {
|
||||||
|
if text.starts_with("--") {
|
||||||
|
"-\u{200B}-".to_string() + &text[2..].replace("\n--", "\n-\u{200B}-")
|
||||||
|
} else {
|
||||||
|
text.replace("\n--", "\n-\u{200B}-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove standard (RFC 3676, §4.3) footer if it is found.
|
/// Remove standard (RFC 3676, §4.3) footer if it is found.
|
||||||
fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
|
fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
|
||||||
|
let mut nearly_standard_footer = None;
|
||||||
for (ix, &line) in lines.iter().enumerate() {
|
for (ix, &line) in lines.iter().enumerate() {
|
||||||
// quoted-printable may encode `-- ` to `-- =20` which is converted
|
|
||||||
// back to `-- `
|
|
||||||
match line {
|
match line {
|
||||||
|
// some providers encode `-- ` to `-- =20` which results in `-- `
|
||||||
"-- " | "-- " => return &lines[..ix],
|
"-- " | "-- " => return &lines[..ix],
|
||||||
|
// some providers encode `-- ` to `=2D-` which results in only `--`;
|
||||||
|
// use that only when no other footer is found
|
||||||
|
// and if the line before is empty and the line after is not empty
|
||||||
|
"--" => {
|
||||||
|
if (ix == 0 || lines[ix - 1] == "") && ix != lines.len() - 1 && lines[ix + 1] != ""
|
||||||
|
{
|
||||||
|
nearly_standard_footer = Some(ix);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(ix) = nearly_standard_footer {
|
||||||
|
return &lines[..ix];
|
||||||
|
}
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,25 +69,27 @@ pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool) {
|
|||||||
let lines = split_lines(&input);
|
let lines = split_lines(&input);
|
||||||
let (lines, is_forwarded) = skip_forward_header(&lines);
|
let (lines, is_forwarded) = skip_forward_header(&lines);
|
||||||
|
|
||||||
let lines = remove_message_footer(lines);
|
let original_lines = &lines;
|
||||||
let (lines, has_nonstandard_footer) = remove_nonstandard_footer(lines);
|
|
||||||
let (lines, has_bottom_quote) = if !is_chat_message {
|
|
||||||
remove_bottom_quote(lines)
|
|
||||||
} else {
|
|
||||||
(lines, false)
|
|
||||||
};
|
|
||||||
let (lines, has_top_quote) = if !is_chat_message {
|
|
||||||
remove_top_quote(lines)
|
|
||||||
} else {
|
|
||||||
(lines, false)
|
|
||||||
};
|
|
||||||
|
|
||||||
// re-create buffer from the remaining lines
|
let lines = remove_message_footer(lines);
|
||||||
let text = render_message(
|
|
||||||
lines,
|
let text = if is_chat_message {
|
||||||
has_top_quote,
|
render_message(lines, false, false)
|
||||||
has_nonstandard_footer || has_bottom_quote,
|
} else {
|
||||||
);
|
let (lines, has_nonstandard_footer) = remove_nonstandard_footer(lines);
|
||||||
|
let (lines, has_bottom_quote) = remove_bottom_quote(lines);
|
||||||
|
let (lines, has_top_quote) = remove_top_quote(lines);
|
||||||
|
|
||||||
|
if lines.iter().all(|it| it.trim().is_empty()) {
|
||||||
|
render_message(original_lines, false, false)
|
||||||
|
} else {
|
||||||
|
render_message(
|
||||||
|
lines,
|
||||||
|
has_top_quote,
|
||||||
|
has_nonstandard_footer || has_bottom_quote,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
(text, is_forwarded)
|
(text, is_forwarded)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,14 +97,13 @@ pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool) {
|
|||||||
/// Returns message body lines and a boolean indicating whether
|
/// Returns message body lines and a boolean indicating whether
|
||||||
/// a message is forwarded or not.
|
/// a message is forwarded or not.
|
||||||
fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||||
if lines.len() >= 3
|
match lines {
|
||||||
&& lines[0] == "---------- Forwarded message ----------"
|
["---------- Forwarded message ----------", first_line, "", rest @ ..]
|
||||||
&& lines[1].starts_with("From: ")
|
if first_line.starts_with("From: ") =>
|
||||||
&& lines[2].is_empty()
|
{
|
||||||
{
|
(rest, true)
|
||||||
(&lines[3..], true)
|
}
|
||||||
} else {
|
_ => (lines, false),
|
||||||
(lines, false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +184,8 @@ fn render_message(lines: &[&str], is_cut_at_begin: bool, is_cut_at_end: bool) ->
|
|||||||
if is_cut_at_end && (!is_cut_at_begin || !empty_body) {
|
if is_cut_at_end && (!is_cut_at_begin || !empty_body) {
|
||||||
ret += " [...]";
|
ret += " [...]";
|
||||||
}
|
}
|
||||||
ret
|
// redo escaping done by escape_message_footer_marks()
|
||||||
|
ret.replace("\u{200B}", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,6 +234,25 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dont_remove_whole_message() {
|
||||||
|
let input = "\n------\nFailed\n------\n\nUh-oh, this workflow did not succeed!\n\nlots of other text".to_string();
|
||||||
|
let (plain, is_forwarded) = simplify(input, false);
|
||||||
|
assert_eq!(
|
||||||
|
plain,
|
||||||
|
"------\nFailed\n------\n\nUh-oh, this workflow did not succeed!\n\nlots of other text"
|
||||||
|
);
|
||||||
|
assert!(!is_forwarded);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chat_message() {
|
||||||
|
let input = "Hi! How are you?\n\n---\n\nI am good.\n-- \nSent with my Delta Chat Messenger: https://delta.chat".to_string();
|
||||||
|
let (plain, is_forwarded) = simplify(input, true);
|
||||||
|
assert_eq!(plain, "Hi! How are you?\n\n---\n\nI am good.");
|
||||||
|
assert!(!is_forwarded);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_simplify_trim() {
|
fn test_simplify_trim() {
|
||||||
let input = "line1\n\r\r\rline2".to_string();
|
let input = "line1\n\r\r\rline2".to_string();
|
||||||
@@ -248,4 +297,46 @@ mod tests {
|
|||||||
assert_eq!(lines, &["not a quote", "> first", "> second"]);
|
assert_eq!(lines, &["not a quote", "> first", "> second"]);
|
||||||
assert!(!has_top_quote);
|
assert!(!has_top_quote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_escape_message_footer_marks() {
|
||||||
|
let esc = escape_message_footer_marks("--\n--text --in line");
|
||||||
|
assert_eq!(esc, "-\u{200B}-\n-\u{200B}-text --in line");
|
||||||
|
|
||||||
|
let esc = escape_message_footer_marks("--\r\n--text");
|
||||||
|
assert_eq!(esc, "-\u{200B}-\r\n-\u{200B}-text");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_remove_message_footer() {
|
||||||
|
let input = "text\n--\nno footer".to_string();
|
||||||
|
let (plain, _) = simplify(input, true);
|
||||||
|
assert_eq!(plain, "text\n--\nno footer");
|
||||||
|
|
||||||
|
let input = "text\n\n--\n\nno footer".to_string();
|
||||||
|
let (plain, _) = simplify(input, true);
|
||||||
|
assert_eq!(plain, "text\n\n--\n\nno footer");
|
||||||
|
|
||||||
|
let input = "text\n\n-- no footer\n\n".to_string();
|
||||||
|
let (plain, _) = simplify(input, true);
|
||||||
|
assert_eq!(plain, "text\n\n-- no footer");
|
||||||
|
|
||||||
|
let input = "text\n\n--\nno footer\n-- \nfooter".to_string();
|
||||||
|
let (plain, _) = simplify(input, true);
|
||||||
|
assert_eq!(plain, "text\n\n--\nno footer");
|
||||||
|
|
||||||
|
let input = "text\n\n--\ntreated as footer when unescaped".to_string();
|
||||||
|
let (plain, _) = simplify(input.clone(), true);
|
||||||
|
assert_eq!(plain, "text"); // see remove_message_footer() for some explanations
|
||||||
|
let escaped = escape_message_footer_marks(&input);
|
||||||
|
let (plain, _) = simplify(escaped, true);
|
||||||
|
assert_eq!(plain, "text\n\n--\ntreated as footer when unescaped");
|
||||||
|
|
||||||
|
let input = "--\ntreated as footer when unescaped".to_string();
|
||||||
|
let (plain, _) = simplify(input.clone(), true);
|
||||||
|
assert_eq!(plain, ""); // see remove_message_footer() for some explanations
|
||||||
|
let escaped = escape_message_footer_marks(&input);
|
||||||
|
let (plain, _) = simplify(escaped, true);
|
||||||
|
assert_eq!(plain, "--\ntreated as footer when unescaped");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,39 +12,34 @@ use crate::context::Context;
|
|||||||
use crate::events::Event;
|
use crate::events::Event;
|
||||||
use crate::login_param::{dc_build_tls, LoginParam};
|
use crate::login_param::{dc_build_tls, LoginParam};
|
||||||
use crate::oauth2::*;
|
use crate::oauth2::*;
|
||||||
|
use crate::stock::StockMessage;
|
||||||
|
|
||||||
/// SMTP write and read timeout in seconds.
|
/// SMTP write and read timeout in seconds.
|
||||||
const SMTP_TIMEOUT: u64 = 30;
|
const SMTP_TIMEOUT: u64 = 30;
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[fail(display = "Bad parameters")]
|
#[error("Bad parameters")]
|
||||||
BadParameters,
|
BadParameters,
|
||||||
|
|
||||||
#[fail(display = "Invalid login address {}: {}", address, error)]
|
#[error("Invalid login address {address}: {error}")]
|
||||||
InvalidLoginAddress {
|
InvalidLoginAddress {
|
||||||
address: String,
|
address: String,
|
||||||
#[cause]
|
#[source]
|
||||||
error: error::Error,
|
error: error::Error,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[fail(display = "SMTP failed to connect: {:?}", _0)]
|
#[error("SMTP: failed to connect: {0:?}")]
|
||||||
ConnectionFailure(#[cause] smtp::error::Error),
|
ConnectionFailure(#[source] smtp::error::Error),
|
||||||
|
|
||||||
#[fail(display = "SMTP: failed to setup connection {:?}", _0)]
|
#[error("SMTP: failed to setup connection {0:?}")]
|
||||||
ConnectionSetupFailure(#[cause] smtp::error::Error),
|
ConnectionSetupFailure(#[source] smtp::error::Error),
|
||||||
|
|
||||||
#[fail(display = "SMTP: oauth2 error {:?}", _0)]
|
#[error("SMTP: oauth2 error {address}")]
|
||||||
Oauth2Error { address: String },
|
Oauth2Error { address: String },
|
||||||
|
|
||||||
#[fail(display = "TLS error")]
|
#[error("TLS error")]
|
||||||
Tls(#[cause] async_native_tls::Error),
|
Tls(#[from] async_native_tls::Error),
|
||||||
}
|
|
||||||
|
|
||||||
impl From<async_native_tls::Error> for Error {
|
|
||||||
fn from(err: async_native_tls::Error) -> Error {
|
|
||||||
Error::Tls(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
@@ -172,7 +167,18 @@ impl Smtp {
|
|||||||
.timeout(Some(Duration::from_secs(SMTP_TIMEOUT)));
|
.timeout(Some(Duration::from_secs(SMTP_TIMEOUT)));
|
||||||
|
|
||||||
let mut trans = client.into_transport();
|
let mut trans = client.into_transport();
|
||||||
trans.connect().await.map_err(Error::ConnectionFailure)?;
|
if let Err(err) = trans.connect().await {
|
||||||
|
let message = context
|
||||||
|
.stock_string_repl_str2(
|
||||||
|
StockMessage::ServerResponse,
|
||||||
|
format!("SMTP {}:{}", domain, port),
|
||||||
|
err.to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
emit_event!(context, Event::ErrorNetwork(message));
|
||||||
|
return Err(Error::ConnectionFailure(err));
|
||||||
|
}
|
||||||
|
|
||||||
self.transport = Some(trans);
|
self.transport = Some(trans);
|
||||||
self.last_success = Some(Instant::now());
|
self.last_success = Some(Instant::now());
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ use crate::events::Event;
|
|||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[fail(display = "Envelope error: {}", _0)]
|
#[error("Envelope error: {}", _0)]
|
||||||
EnvelopeError(#[cause] async_smtp::error::Error),
|
EnvelopeError(#[from] async_smtp::error::Error),
|
||||||
|
|
||||||
#[fail(display = "Send error: {}", _0)]
|
#[error("Send error: {}", _0)]
|
||||||
SendError(#[cause] async_smtp::smtp::error::Error),
|
SendError(#[from] async_smtp::smtp::error::Error),
|
||||||
|
|
||||||
#[fail(display = "SMTP has no transport")]
|
#[error("SMTP has no transport")]
|
||||||
NoTransport,
|
NoTransport,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
101
src/sql.rs
101
src/sql.rs
@@ -27,50 +27,28 @@ macro_rules! paramsv {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[fail(display = "Sqlite Error: {:?}", _0)]
|
#[error("Sqlite Error: {0:?}")]
|
||||||
Sql(#[cause] rusqlite::Error),
|
Sql(#[from] rusqlite::Error),
|
||||||
#[fail(display = "Sqlite Connection Pool Error: {:?}", _0)]
|
#[error("Sqlite Connection Pool Error: {0:?}")]
|
||||||
ConnectionPool(#[cause] r2d2::Error),
|
ConnectionPool(#[from] r2d2::Error),
|
||||||
#[fail(display = "Sqlite: Connection closed")]
|
#[error("Sqlite: Connection closed")]
|
||||||
SqlNoConnection,
|
SqlNoConnection,
|
||||||
#[fail(display = "Sqlite: Already open")]
|
#[error("Sqlite: Already open")]
|
||||||
SqlAlreadyOpen,
|
SqlAlreadyOpen,
|
||||||
#[fail(display = "Sqlite: Failed to open")]
|
#[error("Sqlite: Failed to open")]
|
||||||
SqlFailedToOpen,
|
SqlFailedToOpen,
|
||||||
#[fail(display = "{:?}", _0)]
|
#[error("{0}")]
|
||||||
Io(#[cause] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[fail(display = "{:?}", _0)]
|
#[error("{0:?}")]
|
||||||
BlobError(#[cause] crate::blob::BlobError),
|
BlobError(#[from] crate::blob::BlobError),
|
||||||
|
#[error("{0}")]
|
||||||
|
Other(#[from] crate::error::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
impl From<rusqlite::Error> for Error {
|
|
||||||
fn from(err: rusqlite::Error) -> Error {
|
|
||||||
Error::Sql(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<r2d2::Error> for Error {
|
|
||||||
fn from(err: r2d2::Error) -> Error {
|
|
||||||
Error::ConnectionPool(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<std::io::Error> for Error {
|
|
||||||
fn from(err: std::io::Error) -> Error {
|
|
||||||
Error::Io(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::blob::BlobError> for Error {
|
|
||||||
fn from(err: crate::blob::BlobError) -> Error {
|
|
||||||
Error::BlobError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A wrapper around the underlying Sqlite3 object.
|
/// A wrapper around the underlying Sqlite3 object.
|
||||||
#[derive(DebugStub)]
|
#[derive(DebugStub)]
|
||||||
pub struct Sql {
|
pub struct Sql {
|
||||||
@@ -107,11 +85,13 @@ impl Sql {
|
|||||||
pub async fn open<T: AsRef<Path>>(&self, context: &Context, dbfile: T, readonly: bool) -> bool {
|
pub async fn open<T: AsRef<Path>>(&self, context: &Context, dbfile: T, readonly: bool) -> bool {
|
||||||
match open(context, self, dbfile, readonly).await {
|
match open(context, self, dbfile, readonly).await {
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
Err(crate::error::Error::SqlError(Error::SqlAlreadyOpen)) => false,
|
Err(err) => match err.downcast_ref::<Error>() {
|
||||||
Err(_) => {
|
Some(Error::SqlAlreadyOpen) => false,
|
||||||
self.close().await;
|
_ => {
|
||||||
false
|
self.close().await;
|
||||||
}
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +237,28 @@ impl Sql {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute a query which is expected to return zero or one row.
|
||||||
|
pub async fn query_row_optional<T, F>(
|
||||||
|
&self,
|
||||||
|
sql: impl AsRef<str>,
|
||||||
|
params: Vec<&dyn crate::ToSql>,
|
||||||
|
f: F,
|
||||||
|
) -> Result<Option<T>>
|
||||||
|
where
|
||||||
|
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||||
|
{
|
||||||
|
match self.query_row(sql, params, f).await {
|
||||||
|
Ok(res) => Ok(Some(res)),
|
||||||
|
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
|
||||||
|
Err(Error::Sql(rusqlite::Error::InvalidColumnType(
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
rusqlite::types::Type::Null,
|
||||||
|
))) => Ok(None),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Executes a query which is expected to return one row and one
|
/// Executes a query which is expected to return one row and one
|
||||||
/// column. If the query does not return a value or returns SQL
|
/// column. If the query does not return a value or returns SQL
|
||||||
/// `NULL`, returns `Ok(None)`.
|
/// `NULL`, returns `Ok(None)`.
|
||||||
@@ -268,19 +270,8 @@ impl Sql {
|
|||||||
where
|
where
|
||||||
T: rusqlite::types::FromSql,
|
T: rusqlite::types::FromSql,
|
||||||
{
|
{
|
||||||
match self
|
self.query_row_optional(query, params, |row| row.get::<_, T>(0))
|
||||||
.query_row(query, params, |row| row.get::<_, T>(0))
|
|
||||||
.await
|
.await
|
||||||
{
|
|
||||||
Ok(res) => Ok(Some(res)),
|
|
||||||
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
|
|
||||||
Err(Error::Sql(rusqlite::Error::InvalidColumnType(
|
|
||||||
_,
|
|
||||||
_,
|
|
||||||
rusqlite::types::Type::Null,
|
|
||||||
))) => Ok(None),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Not resultified version of `query_get_value_result`. Returns
|
/// Not resultified version of `query_get_value_result`. Returns
|
||||||
@@ -297,7 +288,7 @@ impl Sql {
|
|||||||
match self.query_get_value_result(query, params).await {
|
match self.query_get_value_result(query, params).await {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(context, "sql: Failed query_row: {}", err);
|
warn!(context, "sql: Failed query_row: {}", err);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1332,6 +1323,8 @@ async fn open(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes from the database locally deleted messages that also don't
|
||||||
|
/// have a server UID.
|
||||||
async fn prune_tombstones(context: &Context) -> Result<()> {
|
async fn prune_tombstones(context: &Context) -> Result<()> {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
|
|||||||
24
src/stock.rs
24
src/stock.rs
@@ -10,7 +10,7 @@ use crate::chat;
|
|||||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||||
use crate::contact::*;
|
use crate::contact::*;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::error::Error;
|
use crate::error::{bail, Error};
|
||||||
use crate::message::Message;
|
use crate::message::Message;
|
||||||
use crate::param::Param;
|
use crate::param::Param;
|
||||||
use crate::stock::StockMessage::{DeviceMessagesHint, WelcomeMessage};
|
use crate::stock::StockMessage::{DeviceMessagesHint, WelcomeMessage};
|
||||||
@@ -35,12 +35,6 @@ pub enum StockMessage {
|
|||||||
#[strum(props(fallback = "Draft"))]
|
#[strum(props(fallback = "Draft"))]
|
||||||
Draft = 3,
|
Draft = 3,
|
||||||
|
|
||||||
#[strum(props(fallback = "%1$s member(s)"))]
|
|
||||||
Member = 4,
|
|
||||||
|
|
||||||
#[strum(props(fallback = "%1$s contact(s)"))]
|
|
||||||
Contact = 6,
|
|
||||||
|
|
||||||
#[strum(props(fallback = "Voice message"))]
|
#[strum(props(fallback = "Voice message"))]
|
||||||
VoiceMessage = 7,
|
VoiceMessage = 7,
|
||||||
|
|
||||||
@@ -136,9 +130,6 @@ pub enum StockMessage {
|
|||||||
))]
|
))]
|
||||||
AcSetupMsgBody = 43,
|
AcSetupMsgBody = 43,
|
||||||
|
|
||||||
#[strum(props(fallback = "Messages I sent to myself"))]
|
|
||||||
SelfTalkSubTitle = 50,
|
|
||||||
|
|
||||||
#[strum(props(fallback = "Cannot login as %1$s."))]
|
#[strum(props(fallback = "Cannot login as %1$s."))]
|
||||||
CannotLogin = 60,
|
CannotLogin = 60,
|
||||||
|
|
||||||
@@ -317,7 +308,8 @@ impl Context {
|
|||||||
from_id: u32,
|
from_id: u32,
|
||||||
) -> String {
|
) -> String {
|
||||||
let insert1 = if id == StockMessage::MsgAddMember || id == StockMessage::MsgDelMember {
|
let insert1 = if id == StockMessage::MsgAddMember || id == StockMessage::MsgDelMember {
|
||||||
let contact_id = Contact::lookup_id_by_addr(self, param1.as_ref()).await;
|
let contact_id =
|
||||||
|
Contact::lookup_id_by_addr(self, param1.as_ref(), Origin::Unknown).await;
|
||||||
if contact_id != 0 {
|
if contact_id != 0 {
|
||||||
Contact::get_by_id(self, contact_id)
|
Contact::get_by_id(self, contact_id)
|
||||||
.await
|
.await
|
||||||
@@ -449,9 +441,9 @@ mod tests {
|
|||||||
// uses %1$s substitution
|
// uses %1$s substitution
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
t.ctx
|
t.ctx
|
||||||
.stock_string_repl_str(StockMessage::Member, "42")
|
.stock_string_repl_str(StockMessage::MsgAddMember, "Foo")
|
||||||
.await,
|
.await,
|
||||||
"42 member(s)"
|
"Member Foo added."
|
||||||
);
|
);
|
||||||
// We have no string using %1$d to test...
|
// We have no string using %1$d to test...
|
||||||
}
|
}
|
||||||
@@ -460,8 +452,10 @@ mod tests {
|
|||||||
async fn test_stock_string_repl_int() {
|
async fn test_stock_string_repl_int() {
|
||||||
let t = dummy_context().await;
|
let t = dummy_context().await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
t.ctx.stock_string_repl_int(StockMessage::Member, 42).await,
|
t.ctx
|
||||||
"42 member(s)"
|
.stock_string_repl_int(StockMessage::MsgAddMember, 42)
|
||||||
|
.await,
|
||||||
|
"Member 42 added."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
test-data/image/avatar64x64.png
Normal file
BIN
test-data/image/avatar64x64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Reference in New Issue
Block a user