Compare commits

...

73 Commits

Author SHA1 Message Date
B. Petersen
b21508fdb7 bump version to 1.50 2020-11-19 14:06:53 +01:00
B. Petersen
92175b27ab update changelog for 1.50 2020-11-19 14:06:52 +01:00
bjoern
4d2a39febb Merge pull request #2087 from deltachat/imbox-dont-fetch-old-msgs
Don't fetch messages that arrived in the meantime if InboxWatch is disabled and re-enabled
2020-11-18 14:22:39 +01:00
bjoern
4fa667d834 Merge pull request #2084 from deltachat/alphaversions
let core carry a 1.50.0-alpha.0 version
2020-11-18 12:27:32 +01:00
Hocuri
38ed94367c Update src/config.rs
Co-authored-by: bjoern <r10s@b44t.com>
2020-11-18 12:08:28 +01:00
Hocuri
3a7bd8b49d Don't fetch messages that arrived in the meantime if InboxWatch is
disabled and re-enabled

That's another narrow-stitching patch for a scenario where many emails could
be deleted all at once and surprisingly: user disables inbox-watch, enables delete-from-server, after a moth enables inbox-watch again -> currently all emails that arrived in the meantime will be deleted (if emails are not fetched, they won't be deleted)
2020-11-17 14:13:27 +01:00
Hocuri
c8d4eee794 Don't fetch from INBOX if it is disabled
Before, if there were more than 20 jobs at once, we unconditionally
fetched from inbox
2020-11-16 17:03:52 +01:00
holger krekel
d66174e55a let core carry a 1.50.0-alpha.0 version (which sits between 1.49.X and 1.50.0).
This way we can better distinguish tagged from untagged core releases, also in logs etc.
We might, from time to time, increase the alpha.N "N" number if we are entering testing etc.
2020-11-16 16:12:55 +01:00
holger krekel
8d2a5cd242 fix link 2020-11-16 11:47:48 +01:00
holger krekel
2fbef80df8 detailing/rewriting the group-sync draft a little. 2020-11-16 11:47:48 +01:00
link2xt
07768133d5 Merge pull request #2083 from deltachat/smtp-plain
smtp: do not use STARTTLS when PLAIN connection is requested
2020-11-15 23:37:41 +03:00
Alexander Krotov
9df88745dc smtp: do not use STARTTLS when PLAIN connection is requested
Also do not allow downgrade if STARTTLS is not available
2020-11-15 22:40:41 +03:00
B. Petersen
bd856d90db remove unused AccountConfig::name
the field was never set or read.

to get the name for an account,
use `dc_get_config(account, "displayname")`.
2020-11-10 03:52:09 +03:00
bjoern
1f4403d149 Merge pull request #2071 from deltachat/prep-1.49
prep 1.49
2020-11-09 15:40:41 +01:00
B. Petersen
6345e57720 bump version to 1.49 2020-11-09 14:11:01 +01:00
B. Petersen
332f32e799 update changelog for 1.49 2020-11-09 14:10:07 +01:00
Alexander Krotov
deb506cb52 Add timestamps to images and videos
It is already done for voice messages and makes saving attachments to
one folder easier.
2020-11-08 22:16:42 +03:00
Alexander Krotov
66907c17d3 mimeparser: preserve quotes in messages with attachments 2020-11-08 12:01:35 +03:00
Alexander Krotov
bf82dd9c60 Document account_id parameter for dc_accounts_remove_account 2020-11-08 01:12:57 +03:00
Alexander Krotov
46c544a5ca deltachat-ffi: forbid quoting messages from another context 2020-11-05 05:57:19 +03:00
Alexander Krotov
c53c6cdf90 deltachat.h documentation fixes 2020-11-01 01:33:15 +03:00
bjoern
c6c2fb562e Merge pull request #2043 from deltachat/prep-1.48
prepare 1.48
2020-10-31 23:12:23 +01:00
B. Petersen
d30bedda96 update changelog to most recent changes 2020-10-31 22:16:33 +01:00
B. Petersen
f7a4f5debf correct/enhance 1.47 changelog 2020-10-31 15:52:59 +01:00
B. Petersen
ea759f17d0 bump version to 1.48 2020-10-31 15:52:59 +01:00
B. Petersen
236faafe0f update changelog for 1.48 2020-10-31 15:52:59 +01:00
bjoern
769f9af861 Merge pull request #2056 from deltachat/smtp-multi-send
send messages via SMTP in configurable chunks
2020-10-31 15:44:32 +01:00
B. Petersen
bf83f6d4ad read settings from provider-db-globals, not from -config-defaults 2020-10-31 14:20:16 +01:00
B. Petersen
8232a148aa send messages via SMTP in configurable chunks
providers may have a maximum for the SMTP RCPT TO: header,
therefore, an outgoing message is split into several chunks.
the chunk size defaults to 50, however, if known to be larger or smaller
by a dedicated provider, this setting may be changed.

this does not affect the MIME To: headers,
if a provider has a maximum here, this is change would not help.
2020-10-31 14:20:16 +01:00
bjoern
04a4424664 Merge pull request #2059 from deltachat/outdated-test
fix outdated test
2020-10-31 14:18:34 +01:00
B. Petersen
da729a8345 fix outdated test
the test just did not work on the last day of a month.
2020-10-31 14:06:17 +01:00
bjoern
25513a6e42 Merge pull request #2050 from deltachat/restore-saved-messages-hint
add hint about how to restore saved-messages chat
2020-10-31 11:59:04 +01:00
bjoern
9063725729 Merge pull request #2057 from deltachat/fix-mistakenly-unarchive
fix mistakenly unarchive
2020-10-31 11:52:34 +01:00
B. Petersen
46833ca4f2 do not unarchive one-to-one on receiving read-receipts 2020-10-30 19:03:02 +01:00
B. Petersen
dbdea787a7 fix typo that results in a not-working test 2020-10-30 19:02:10 +01:00
Alexander Krotov
5c1bbc5d6a Cancel ephemeral task in Context.stop_io()
ephemeral_task holds a reference to Context, preventing event emitter
from returning NULL and terminating event loop. Prior to this change,
there was no way to quickly terminate pending ephemeral_task.
2020-10-28 01:10:23 +03:00
Alexander Krotov
f30c319fbf Remove trailing space in Python code 2020-10-27 23:27:00 +03:00
B. Petersen
b2b59852a7 adapt test for updating device chats 2020-10-27 14:30:23 +01:00
B. Petersen
4c4a9b52de show hint how to restore saved-messages
the saved-messages can be deleted as any other chat;
if the feature is unneeded, this probably also makes some sense.

however, if saved-messages was deleted accidentally (yeah, i've seen that :),
it is not intuitive to see how the saved-messages feature can be restored.

this change just drops a note about how-to-restore to the device chat.
2020-10-27 14:26:23 +01:00
Hocuri
c8242b12fe Add docs for clone_online_account 2020-10-27 08:00:26 +01:00
holger krekel
622d99a971 remove option<path> from inner/imex handling to simplify the code 2020-10-26 20:34:52 +01:00
holger krekel
45ea41262c fix export/import self-key roundtrip 2020-10-26 20:34:52 +01:00
holger krekel
ed5167babc fix export of self private keys 2020-10-26 20:34:52 +01:00
Alexander Krotov
11d9fcad35 Display a quote if top posting is detected
Previously quote at the end was always displayed as [...].
2020-10-26 18:03:30 +03:00
bjoern
fa7b6c001e Merge pull request #2046 from deltachat/fix2026
remove reference to star-commands.
2020-10-25 19:32:40 +01:00
holger krekel
42bd1bc806 remove reference to star-commands. 2020-10-25 18:53:55 +01:00
Hocuri
31bf34890a Always show the cause for which a msg landed in trash 2020-10-25 17:42:26 +01:00
Hocuri
34af492afb Rename fetch_existing to fetch_existing_msgs, add comment (#2042) 2020-10-24 12:10:36 +02:00
holger krekel
ef245b5759 reduce code a bit and shortcut contact lookup to make it easier to remove an existing contact whether blocked or not. 2020-10-23 21:10:12 +02:00
Hocuri
41b2dee4ca Use get_contact() instead of create_contact() 2020-10-23 21:10:12 +02:00
adbenitez
1ce1a01d49 improve docstring 2020-10-23 21:10:12 +02:00
adbenitez
1cd3ee6a05 add failing test 2020-10-23 21:10:12 +02:00
B. Petersen
75df8f762c update provider-db 2020-10-23 17:19:47 +02:00
Hocuri
e5da5c48f1 second review 2020-10-23 17:19:23 +02:00
Hocuri
5b5c6a9c31 Alex' review 2020-10-23 17:19:23 +02:00
Hocuri
4ae1a17cc0 Add test for "Saved-messages do not pop up in original chat in multi-device anymore" 2020-10-23 17:19:23 +02:00
bjoern
0781316c97 Merge pull request #2040 from deltachat/workaround-spawned-tasks
workaround executer-blocking-handling bug
2020-10-23 16:20:19 +02:00
B. Petersen
8eb73a5ade workaround executer-blocking-handling-bug in async-std 2020-10-23 16:04:36 +02:00
Hocuri
e6b7a7e292 saved-messages do not pop up in original chat in multi-device anymore
fix #2020

When forwarding a message, the original `in_reply_to` stays in place.
I did not find any recent commit that changed anything about this,
but IIRC there was a bug that prevented setting the `in_reply_to` at
all.

So, for chat messages, do not look at the InReplyTo header (and also not
at the References header)
2020-10-23 12:33:25 +03:00
Hocuri
c438691b73 Disable fetch-existing for now 2020-10-23 08:31:49 +02:00
adbenitez
1cacfb30ff avoid usage of deprecated contact.set_blocked() 2020-10-23 08:25:46 +02:00
Simon Laux
39a00929c7 fix function reference in changelog 2020-10-21 18:29:00 +03:00
Alexander Krotov
8156692e5a configure: always try at least one IMAP and SMTP server
Previously empty autoconfig resulted in no servers being tried and no
error displayed.
2020-10-21 02:30:16 +03:00
holger krekel
5a9a4dbbab introduce Account.get_blocked_contacts
also introduce Contact.block() and Contact.unblock() methods
and deprecate the c-ish "Contact.set_blocked()" api.
fixes #2011
2020-10-20 11:56:02 +02:00
bjoern
df56b76182 Update CHANGELOG.md
Co-authored-by: Hocuri <hocuri@gmx.de>
2020-10-19 18:44:39 +02:00
B. Petersen
0fc1134bab bump version to 1.47 2020-10-19 18:44:39 +02:00
B. Petersen
dfecd033a7 update changelog for 1.47 2020-10-19 18:44:39 +02:00
Hocuri
6c5eaaed2c Don't peek-receipients/fetch-existing if this is a bot 2020-10-19 18:10:54 +02:00
Hocuri
dcc00075b0 extract variable 2020-10-19 15:23:18 +02:00
Hocuri
a320fb9d6c Doesn't look great, but this was the only way to make compiler happy 2020-10-19 15:23:18 +02:00
Hocuri
3bef4909d5 Update src/message.rs
Co-authored-by: holger krekel  <holger@merlinux.eu>
2020-10-19 15:23:18 +02:00
Hocuri
26aeacc6be @link2xt's review 2020-10-19 15:23:18 +02:00
Hocuri
0b1288fc17 Mark all failed messages as failed when receiving an NDN
There may be multiple messages with the same `Message-Id`-header alias
rfc724_mid because an email with multiple attachments was split up.
2020-10-19 15:23:18 +02:00
40 changed files with 1038 additions and 495 deletions

View File

@@ -1,10 +1,134 @@
# Changelog
## Unreleased
## 1.50.0
- breaking change: `dc_update_device_chats()` was removed. This is now done automatically during configure.
- do not fetch emails in between inbox_watch disabled and enabled again #2087
- fix: do not fetch from INBOX if inbox_watch is disabled #2085
- fix: do not use STARTTLS when PLAIN connection is requested
and do not allow downgrade if STARTTLS is not available #2071
## 1.49.0
- add timestamps to image and video filenames #2068
- forbid quoting messages from another context #2069
- fix: preserve quotes in messages with attachments #2070
## 1.48.0
- `fetch_existing` renamed to `fetch_existing_msgs` and disabled by default
#2035 #2042
- skip fetch existing messages/contacts if config-option `bot` set #2017
- always log why a message is sorted to trash #2045
- display a quote if top posting is detected #2047
- add ephemeral task cancellation to `dc_stop_io()`;
before, there was no way to quickly terminate pending ephemeral tasks #2051
- when saved-messages chat is deleted,
a device-message about recreation is added #2050
- use `max_smtp_rcpt_to` from provider-db,
sending messages to many recipients in configurable chunks #2056
- fix handling of empty autoconfigure files #2027
- fix adding saved messages to wrong chats on multi-device #2034 #2039
- fix hang on android4.4 and other systems
by adding a workaround to executer-blocking-handling bug #2040
- fix secret key export/import roundtrip #2048
- fix mistakenly unarchived chats #2057
- fix outdated-reminder test that fails only 7 days a year,
including halloween :) #2059
- improve python bindings #2021 #2036 #2038
- update provider-database #2037
## 1.47.0
- breaking change: `dc_update_device_chats()` removed;
this is now done automatically during configure
unless the new config-option `bot` is set #1957
- breaking change: split `DC_EVENT_MSGS_NOTICED` off `DC_EVENT_MSGS_CHANGED`
and remove `dc_marknoticed_all_chats()` #1942 #1981
- breaking change: remove unused starring options #1965
- breaking change: `DC_CHAT_TYPE_VERIFIED_GROUP` replaced by
`dc_chat_is_protected()`; also single-chats may be protected now, this may
happen over the wire even if the UI do not offer an option for that #1968
- breaking change: split quotes off message text,
UIs should use at least `dc_msg_get_quoted_text()` to show quotes now #1975
- new api for quote handling: `dc_msg_set_quote()`, `dc_msg_get_quoted_text()`,
`dc_msg_get_quoted_msg()` #1975 #1984 #1985 #1987 #1989 #2004
- require quorum to enable encryption #1946
- speed up and clean up account creation #1912 #1927 #1960 #1961
- configure now collects recent contacts and fetches last messages
unless disabled by `fetch_existing` config-option #1913 #2003
EDIT: `fetch_existing` renamed to `fetch_existing_msgs` in 1.48.0 #2042
- emit `DC_EVENT_CHAT_MODIFIED` on contact rename
and set contact-id on `DC_EVENT_CONTACTS_CHANGED` #1935 #1936 #1937
- add `dc_set_chat_protection()`; the `protect` parameter in
`dc_create_group_chat()` will be removed in an upcoming release;
up to then, UIs using the "verified group" paradigm
should not use `dc_set_chat_protection()` #1968 #2014 #2001 #2012 #2007
- remove unneeded `DC_STR_COUNT` #1991
- mark all failed messages as failed when receiving an NDN #1993
- check some easy cases for bad system clock and outdated app #1901
- fix import temporary directory usage #1929
- fix forcing encryption for reset peers #1998
- fix: do not allow to save drafts in non-writeable chats #1997
- fix: do not show HTML if there is no content and there is an attachment #1988
- fix recovering offline/lost connections, fixes background receive bug #1983
- fix ordering of accounts returned by `dc_accounts_get_all()` #1909
- fix whitespace for summaries #1938
- fix: improve sentbox name guessing #1941
- fix: avoid manual poll impl for accounts events #1944
- fix encoding newlines in param as a preparation for storing quotes #1945
- fix: internal and ffi error handling #1967 #1966 #1959 #1911 #1916 #1917 #1915
- fix ci #1928 #1931 #1932 #1933 #1934 #1943
- update provider-database #1940 #2005 #2006
- update dependencies #1919 #1908 #1950 #1963 #1996 #2010 #2013
- Added a `bot` config. Currently, it only prevents filling the device chats automatically.
## 1.46.0
@@ -751,4 +875,3 @@
For a full list of changes, please see our closed Pull Requests:
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed

4
Cargo.lock generated
View File

@@ -1003,7 +1003,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.46.0"
version = "1.50.0"
dependencies = [
"ansi_term 0.12.1",
"anyhow",
@@ -1079,7 +1079,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.46.0"
version = "1.50.0"
dependencies = [
"anyhow",
"async-std",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.46.0"
version = "1.50.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"

View File

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

View File

@@ -36,16 +36,18 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
*
* First of all, you have to **create a context object**
* bound to a database.
* The database is a normal sqlite-file and is created as needed:
* The database is a normal SQLite file with a "blob directory" beside it.
* This will create "example.db" database and "example.db-blobs"
* directory if they don't exist already:
*
* ~~~
* dc_context_t* context = dc_context_new(NULL, "example.db", NULL);
* ~~~
*
* After that, make sure, you can **receive events from the context**.
* After that, make sure you can **receive events from the context**.
* For that purpose, create an event emitter you can ask for events.
* If there are no event, the emitter will wait until there is one,
* so, in many situations you will do this in a thread:
* If there is no event, the emitter will wait until there is one,
* so, in many situations, you will do this in a thread:
*
* ~~~
* void* event_handler(void* context)
@@ -66,7 +68,7 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
*
* The example above uses "pthreads",
* however, you can also use anything else for thread handling.
* All deltachat-core-functions, unless stated otherwise, are thread-safe.
* All deltachat-core functions, unless stated otherwise, are thread-safe.
*
* Now you can **configure the context:**
*
@@ -77,13 +79,13 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
* dc_configure(context);
* ~~~
*
* dc_configure() returns immediately,
* the configuration itself runs in background and may take a while.
* dc_configure() returns immediately.
* The configuration itself runs in the background and may take a while.
* Once done, the #DC_EVENT_CONFIGURE_PROGRESS reports success
* to the event_handler() you've defined above.
*
* The configuration result is saved in the database,
* on subsequent starts it is not needed to call dc_configure()
* The configuration result is saved in the database.
* On subsequent starts it is not needed to call dc_configure()
* (you can check this using dc_is_configured()).
*
* On a successfully configured context,
@@ -105,12 +107,12 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
*
* dc_send_text_msg() returns immediately;
* the sending itself is done in the background.
* If you check the testing address (bob)
* and you should have received a normal email.
* Answer this email in any email program with "Got it!"
* If you check the testing address (bob),
* you should receive a normal email.
* Answer this email in any email program with "Got it!",
* and the IO you started above will **receive the message**.
*
* You can then **list all messages** of a chat as follow:
* You can then **list all messages** of a chat as follows:
*
* ~~~
* dc_array_t* msglist = dc_get_chat_msgs(context, chat_id, 0, 0);
@@ -181,18 +183,12 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
* opened, connected and mails are fetched.
*
* @memberof dc_context_t
* @param os_name is only for decorative use
* and is shown eg. in the `X-Mailer:` header
* in the form "Delta Chat Core <version>/<os_name>".
* @param os_name is only for decorative use.
* You can give the name of the app, the operating system,
* the used environment and/or the version here.
* It is okay to give NULL, in this case `X-Mailer:` header
* is set to "Delta Chat Core <version>".
* @param dbfile The file to use to store the database,
* something like `~/file` won't work, use absolute paths.
* @param blobdir A directory to store the blobs in; a trailing slash is not needed.
* If you pass NULL or the empty string, deltachat-core creates a directory
* beside _dbfile_ with the same name and the suffix `-blobs`.
* @param blobdir Deprecated, pass NULL or an empty string here.
* @return A context object with some public members.
* The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage.
@@ -205,14 +201,9 @@ dc_context_t* dc_context_new (const char* os_name, const char* d
/**
* Free a context object.
* If app runs can only be terminated by a forced kill, this may be superfluous.
* Before the context object is freed, connections to SMTP, IMAP and database
* are closed. You can also do this explicitly by calling dc_close() on your own
* before calling dc_context_unref().
*
* You have to call this function
* also for accounts returned by dc_accounts_get_account() or dc_accounts_get_selected_account(),
* however, in this case, the context is not shut down but just a reference counter is decreased
* also for accounts returned by dc_accounts_get_account() or dc_accounts_get_selected_account().
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new(),
@@ -230,7 +221,7 @@ void dc_context_unref (dc_context_t* context);
* If the context was created by dc_context_new(), a random ID is assigned.
*
* @memberof dc_context_t
* @param context The context object as created eg. by dc_accounts_get_account() or dc_context_new().
* @param context The context object as created e.g. by dc_accounts_get_account() or dc_context_new().
* @return The context-id.
*/
uint32_t dc_get_id (dc_context_t* context);
@@ -238,7 +229,7 @@ uint32_t dc_get_id (dc_context_t* context);
/**
* Create the event emitter that is used to receive events.
* The library will emit various @ref DC_EVENT events as "new message", "message read" etc.
* The library will emit various @ref DC_EVENT events, such as "new message", "message read" etc.
* To get these events, you have to create an event emitter using this function
* and call dc_get_next_event() on the emitter.
*
@@ -249,7 +240,7 @@ uint32_t dc_get_id (dc_context_t* context);
*
* Note: Use only one event emitter per context.
* Having more than one event emitter running at the same time on the same context
* will result in events randomly delivered to the one or to the other.
* will result in events being randomly delivered to one of the emitters.
*/
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
@@ -350,7 +341,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* The type `jitsi:` may be handled by external apps.
* If no type is prefixed, the videochat is handled completely in a browser.
* - `bot` = Set to "1" if this is a bot. E.g. prevents adding the "Device messages" and "Saved messages" chats.
* - `fetch_existing` = 1=fetch most recent existing messages on configure (default),
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
*
@@ -474,7 +465,7 @@ char* dc_get_oauth2_url (dc_context_t* context, const char*
/**
* Configure a context.
* While configuration IO must not be started, if needed stop IO using dc_stop_io() first.
* During configuration IO must not be started, if needed stop IO using dc_stop_io() first.
* If the context is already configured,
* this function will try to change the configuration.
*
@@ -562,9 +553,12 @@ void dc_start_io (dc_context_t* context);
int dc_is_io_running(const dc_context_t* context);
/**
* Stop job and IMAP/SMTP tasks and return when they are finished.
* If IO is not running, nothing happens.
* Stop job, IMAP, SMTP and other tasks and return when they
* are finished.
* To check the current IO state, use dc_is_io_running().
* Even if IO is not running, there may be pending tasks,
* so this function should always be called before releasing
* context to ensure clean termination of event loop.
*
* If the context was created by the dc_accounts_t account manager,
* use dc_accounts_stop_io() instead of this function.
@@ -577,7 +571,7 @@ void dc_stop_io(dc_context_t* context);
/**
* This function should be called when there is a hint
* that the network is available again,
* eg. as a response to system event reporting network availability.
* e.g. as a response to system event reporting network availability.
* The library will try to send pending messages out immediately.
*
* Moreover, to have a reliable state
@@ -638,7 +632,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
* Clients should not try to re-sort the list as this would be an expensive action
* and would result in inconsistencies between clients.
*
* To get information about each entry, use eg. dc_chatlist_get_summary().
* To get information about each entry, use e.g. dc_chatlist_get_summary().
*
* By default, the function adds some special entries to the list.
* These special entries can be identified by the ID returned by dc_chatlist_get_chat_id():
@@ -670,7 +664,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
* 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
* to the list (may be used eg. for selecting chats on forwarding, the flag is
* to the list (may be used e.g. for selecting chats on forwarding, the flag is
* not needed when DC_GCL_ARCHIVED_ONLY is already set)
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
* is added as needed.
@@ -722,7 +716,7 @@ uint32_t dc_create_chat_by_msg_id (dc_context_t* context, uint32_t ms
* see dc_create_group_chat().
*
* If a chat already exists, this ID is returned, otherwise a new chat is created;
* this new chat may already contain messages, eg. from the deaddrop, to get the
* this new chat may already contain messages, e.g. from the deaddrop, to get the
* chat messages, use dc_get_chat_msgs().
*
* @memberof dc_context_t
@@ -801,7 +795,7 @@ uint32_t dc_prepare_msg (dc_context_t* context, uint32_t ch
*
* Sends the event #DC_EVENT_MSGS_CHANGED on succcess.
* However, this does not imply, the message really reached the recipient -
* sending may be delayed eg. due to network problems. However, from your
* sending may be delayed e.g. due to network problems. However, from your
* view, you're done with the message. Sooner or later it will find its way.
*
* Example:
@@ -858,7 +852,7 @@ uint32_t dc_send_msg_sync (dc_context_t* context, uint32
*
* Sends the event #DC_EVENT_MSGS_CHANGED on succcess.
* However, this does not imply, the message really reached the recipient -
* sending may be delayed eg. due to network problems. However, from your
* sending may be delayed e.g. due to network problems. However, from your
* view, you're done with the message. Sooner or later it will find its way.
*
* See also dc_send_msg().
@@ -889,7 +883,7 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
* and a url that can be opened in a supported browser to join the videochat
*
* - delta-clients can get all information needed from
* the message object, using eg.
* the message object, using e.g.
* dc_msg_get_videochat_url() and check dc_msg_get_viewtype() for #DC_MSG_VIDEOCHAT_INVITATION
*
* dc_send_videochat_invitation() is blocking and may take a while,
@@ -898,9 +892,9 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
* for this purpose, the function returns the message-id directly.
*
* As for other messages sent, this function
* sends the event #DC_EVENT_MSGS_CHANGED on succcess, the message has a delivery state, and so on.
* sends the event #DC_EVENT_MSGS_CHANGED on success, the message has a delivery state, and so on.
* The recipient will get noticed by the call as usual by #DC_EVENT_INCOMING_MSG or #DC_EVENT_MSGS_CHANGED,
* However, UIs might some things differently, eg. play a different sound.
* However, UIs might some things differently, e.g. play a different sound.
*
* @memberof dc_context_t
* @param context The context object
@@ -921,7 +915,7 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
* allowing the user to continue editing and sending.
*
* Drafts are considered when sorting messages
* and are also returned eg. by dc_chatlist_get_summary().
* and are also returned e.g. by dc_chatlist_get_summary().
*
* Each chat can have its own draft but only one draft per chat is possible.
*
@@ -1169,7 +1163,7 @@ uint32_t dc_get_next_media (dc_context_t* context, uint32_t ms
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to change the protection for.
* @param protect 1=protect chat, 0=unprotect chat
* @return 1=success, 0=error, eg. some members may be unverified
* @return 1=success, 0=error, e.g. some members may be unverified
*/
int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect);
@@ -1424,7 +1418,7 @@ int dc_set_chat_profile_image (dc_context_t* context, uint32_t ch
/**
* Set mute duration of a chat.
*
* The ui can then call dc_chat_is_muted() when receiving a new message to decide whether it should trigger an notification.
* The UI can then call dc_chat_is_muted() when receiving a new message to decide whether it should trigger an notification.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*
@@ -1440,7 +1434,7 @@ int dc_set_chat_mute_duration (dc_context_t* context, ui
/**
* Get an informational text for a single message. The text is multiline and may
* contain eg. the raw text of the message.
* contain e.g. the raw text of the message.
*
* The max. text returned is typically longer (about 100000 characters) than the
* max. text returned by dc_msg_get_text() (about 30000 characters).
@@ -1464,7 +1458,7 @@ char* dc_get_msg_info (dc_context_t* context, uint32_t ms
* @param msg_id The message id, must be the id of an incoming message.
* @return Raw headers as a multi-line string, must be released using dc_str_unref() after usage.
* Returns NULL if there are no headers saved for the given message,
* eg. because of save_mime_headers is not set
* e.g. because of save_mime_headers is not set
* or the message is not incoming.
*/
char* dc_get_mime_headers (dc_context_t* context, uint32_t msg_id);
@@ -1512,7 +1506,7 @@ void dc_marknoticed_contact (dc_context_t* context, uint32_t co
/**
* Mark a message as _seen_, updates the IMAP state and
* sends MDNs. If the message is not in a real chat (eg. a contact request), the
* sends MDNs. If the message is not in a real chat (e.g. a contact request), the
* message is only marked as NOTICED and no IMAP/MDNs is done. See also
* dc_marknoticed_chat() and dc_marknoticed_contact()
*
@@ -1686,7 +1680,7 @@ void dc_block_contact (dc_context_t* context, uint32_t co
/**
* Get encryption info for a contact.
* Get a multi-line encryption info, containing your fingerprint and the
* fingerprint of the contact, used eg. to compare the fingerprints for a simple out-of-band verification.
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
*
* @memberof dc_context_t
* @param context The context object.
@@ -1711,7 +1705,7 @@ int dc_delete_contact (dc_context_t* context, uint32_t co
/**
* Get a single contact object. For a list, see eg. dc_get_contacts().
* Get a single contact object. For a list, see e.g. dc_get_contacts().
*
* For contact DC_CONTACT_ID_SELF (1), the function returns sth.
* like "Me" in the selected language and the email address
@@ -1780,7 +1774,7 @@ void dc_imex (dc_context_t* context, int what, c
/**
* Check if there is a backup file.
* May only be used on fresh installations (eg. dc_is_configured() returns 0).
* May only be used on fresh installations (e.g. dc_is_configured() returns 0).
*
* Example:
*
@@ -1873,7 +1867,7 @@ char* dc_imex_has_backup (dc_context_t* context, const char*
* @memberof dc_context_t
* @param context The context object.
* @return The setup code. Must be released using dc_str_unref() after usage.
* On errors, eg. if the message could not be sent, NULL is returned.
* On errors, e.g. if the message could not be sent, NULL is returned.
*/
char* dc_initiate_key_transfer (dc_context_t* context);
@@ -1896,7 +1890,7 @@ char* dc_initiate_key_transfer (dc_context_t* context);
* There is no need to format the string correctly, the function will remove all spaces and other characters and
* insert the `-` characters at the correct places.
* @return 1=key successfully decrypted and imported; both devices will use the same key now;
* 0=key transfer failed eg. due to a bad setup code.
* 0=key transfer failed e.g. due to a bad setup code.
*/
int dc_continue_key_transfer (dc_context_t* context, uint32_t msg_id, const char* setup_code);
@@ -1967,7 +1961,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
/**
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works eg. with OpenKeychain.
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
*
* The scanning device will pass the scanned content to dc_check_qr() then;
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
@@ -2248,7 +2242,7 @@ void dc_accounts_unref (dc_accounts_t* accounts);
* Internally, dc_context_new() is called using a unique database-name
* in the directory specified at dc_accounts_new().
*
* If the function succceeds,
* If the function succeeds,
* dc_accounts_get_all() will return one more account
* and you can access the newly created account using dc_accounts_get_account().
* Moreover, the newly created account will be the selected one.
@@ -2277,7 +2271,7 @@ uint32_t dc_accounts_import_account (dc_accounts_t* accounts, const
/**
* Migrate independent accounts into accounts managed by the account mangager.
* Migrate independent accounts into accounts managed by the account manager.
* This will _move_ the database-file and all blob-files to the directory managed
* by the account-manager
* (to save disk-space on small devices, the files are not _copied_
@@ -2301,9 +2295,10 @@ uint32_t dc_accounts_migrate_account (dc_accounts_t* accounts, const
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @param account_id The account-id as returned e.g. by dc_accounts_add_account().
* @return 1=success, 0=error
*/
int dc_accounts_remove_account (dc_accounts_t* accounts, uint32_t);
int dc_accounts_remove_account (dc_accounts_t* accounts, uint32_t account_id);
/**
@@ -2322,7 +2317,7 @@ dc_array_t* dc_accounts_get_all (dc_accounts_t* accounts);
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @param account_id The account-id as returned eg. by dc_accounts_get_all() or dc_accounts_add_account().
* @param account_id The account-id as returned e.g. by dc_accounts_get_all() or dc_accounts_add_account().
* @return The account-context, this can be used most similar as a normal,
* unmanaged account-context as created by dc_context_new().
* Once you do no longer need the context-object, you have to call dc_context_unref() on it,
@@ -2353,7 +2348,7 @@ dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @param account_id The account-id as returned eg. by dc_accounts_get_all() or dc_accounts_add_account().
* @param account_id The account-id as returned e.g. by dc_accounts_get_all() or dc_accounts_add_account().
* @return 1=success, 0=error
*/
int dc_accounts_select_account (dc_accounts_t* accounts, uint32_t account_id);
@@ -2373,7 +2368,6 @@ void dc_accounts_start_io (dc_accounts_t* accounts);
/**
* Stop job and IMAP/SMTP tasks for all accounts and return when they are finished.
* If IO is not running, nothing happens.
* This is similar to dc_stop_io(), which, however,
* must not be called for accounts handled by the account manager.
*
@@ -2432,7 +2426,7 @@ dc_accounts_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accou
*
* @memberof dc_array_t
* @param array The array object to free,
* created eg. by dc_get_chatlist(), dc_get_contacts() and so on.
* created e.g. by dc_get_chatlist(), dc_get_contacts() and so on.
* If NULL is given, nothing is done.
*/
void dc_array_unref (dc_array_t* array);
@@ -2630,7 +2624,7 @@ int dc_array_search_id (const dc_array_t* array, uint32_t
* Free a chatlist object.
*
* @memberof dc_chatlist_t
* @param chatlist The chatlist object to free, created eg. by dc_get_chatlist(), dc_search_msgs().
* @param chatlist The chatlist object to free, created e.g. by dc_get_chatlist(), dc_search_msgs().
* If NULL is given, nothing is done.
*/
void dc_chatlist_unref (dc_chatlist_t* chatlist);
@@ -2640,7 +2634,7 @@ void dc_chatlist_unref (dc_chatlist_t* chatlist);
* Find out the number of chats in a chatlist.
*
* @memberof dc_chatlist_t
* @param chatlist The chatlist object as created eg. by dc_get_chatlist().
* @param chatlist The chatlist object as created e.g. by dc_get_chatlist().
* @return Returns the number of items in a dc_chatlist_t object. 0 on errors or if the list is empty.
*/
size_t dc_chatlist_get_cnt (const dc_chatlist_t* chatlist);
@@ -2652,7 +2646,7 @@ size_t dc_chatlist_get_cnt (const dc_chatlist_t* chatlist);
* To get the message object from the message ID, use dc_get_chat().
*
* @memberof dc_chatlist_t
* @param chatlist The chatlist object as created eg. by dc_get_chatlist().
* @param chatlist The chatlist object as created e.g. by dc_get_chatlist().
* @param index The index to get the chat ID for.
* @return Returns the chat_id of the item at the given index. Index must be between
* 0 and dc_chatlist_get_cnt()-1.
@@ -2666,10 +2660,10 @@ uint32_t dc_chatlist_get_chat_id (const dc_chatlist_t* chatlist, siz
* To get the message object from the message ID, use dc_get_msg().
*
* @memberof dc_chatlist_t
* @param chatlist The chatlist object as created eg. by dc_get_chatlist().
* @param chatlist The chatlist object as created e.g. by dc_get_chatlist().
* @param index The index to get the chat ID for.
* @return Returns the message_id of the item at the given index. Index must be between
* 0 and dc_chatlist_get_cnt()-1. If there is no message at the given index (eg. the chat may be empty), 0 is returned.
* 0 and dc_chatlist_get_cnt()-1. If there is no message at the given index (e.g. the chat may be empty), 0 is returned.
*/
uint32_t dc_chatlist_get_msg_id (const dc_chatlist_t* chatlist, size_t index);
@@ -2687,14 +2681,14 @@ uint32_t dc_chatlist_get_msg_id (const dc_chatlist_t* chatlist, siz
* Typically used to show dc_lot_t::text1 with different colors. 0 if not applicable.
*
* - dc_lot_t::text2: contains an excerpt of the message text or strings as
* "No messages". May be NULL of there is no such text (eg. for the archive link)
* "No messages". May be NULL of there is no such text (e.g. for the archive link)
*
* - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable.
*
* - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()). 0 if not applicable.
*
* @memberof dc_chatlist_t
* @param chatlist The chatlist to query as returned eg. from dc_get_chatlist().
* @param chatlist The chatlist to query as returned e.g. from dc_get_chatlist().
* @param index The index to query in the chatlist.
* @param chat To speed up things, pass an already available chat object here.
* If the chat object is not yet available, it is faster to pass NULL.
@@ -2711,7 +2705,7 @@ dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, siz
* as arguments. The chatlist object itself is not needed directly.
*
* This maybe useful if you convert the complete object into a different represenation
* as done eg. in the node-bindings.
* as done e.g. in the node-bindings.
* If you have access to the chatlist object in some way, using this function is not recommended,
* use dc_chatlist_get_summary() in this case instead.
*
@@ -2736,9 +2730,9 @@ dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
/**
* Get info summary for a chat, in json format.
* Get info summary for a chat, in JSON format.
*
* The returned json string has the following key/values:
* The returned JSON string has the following key/values:
*
* id: chat id
* name: chat/group name
@@ -2749,7 +2743,7 @@ dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
* last-message-date:
* avatar-path: path-to-blobfile
* is_verified: yes/no
* @return a utf8-encoded json string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
* @return a UTF8-encoded JSON string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
*/
char* dc_chat_get_info_json (dc_context_t* context, size_t chat_id);
@@ -2757,7 +2751,7 @@ char* dc_chat_get_info_json (dc_context_t* context, size_t chat
* @class dc_chat_t
*
* An object representing a single chat in memory.
* Chat objects are created using eg. dc_get_chat()
* Chat objects are created using e.g. dc_get_chat()
* and are not updated on database changes;
* if you want an update, you have to recreate the object.
*/
@@ -2779,7 +2773,7 @@ char* dc_chat_get_info_json (dc_context_t* context, size_t chat
* Free a chat object.
*
* @memberof dc_chat_t
* @param chat Chat object are returned eg. by dc_get_chat().
* @param chat Chat object are returned e.g. by dc_get_chat().
* If NULL is given, nothing is done.
*/
void dc_chat_unref (dc_chat_t* chat);
@@ -2822,7 +2816,7 @@ int dc_chat_get_type (const dc_chat_t* chat);
/**
* Get name of a chat. For one-to-one chats, this is the name of the contact.
* For group chats, this is the name given eg. to dc_create_group_chat() or
* For group chats, this is the name given e.g. to dc_create_group_chat() or
* received by a group-creation message.
*
* To change the name, use dc_set_chat_name()
@@ -2915,7 +2909,7 @@ int dc_chat_is_self_talk (const dc_chat_t* chat);
* Device-talks contain update information
* and some hints that are added during the program runs, multi-device etc.
*
* From the ui view, device-talks are not very special,
* From the UI view, device-talks are not very special,
* the user can delete and forward messages, archive the chat, set notifications etc.
*
* Messages can be added to the device-talk using dc_add_device_msg()
@@ -2929,10 +2923,10 @@ int dc_chat_is_device_talk (const dc_chat_t* chat);
/**
* Check if messages can be sent to a give chat.
* This is not true eg. for the deaddrop or for the device-talk, cmp. dc_chat_is_device_talk().
* This is not true e.g. for the deaddrop or for the device-talk, cmp. dc_chat_is_device_talk().
*
* Calling dc_send_msg() for these chats will fail
* and the ui may decide to hide input controls therefore.
* and the UI may decide to hide input controls therefore.
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -3018,8 +3012,8 @@ int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
/**
* Create new message object. Message objects are needed eg. for sending messages using
* dc_send_msg(). Moreover, they are returned eg. from dc_get_msg(),
* Create new message object. Message objects are needed e.g. for sending messages using
* dc_send_msg(). Moreover, they are returned e.g. from dc_get_msg(),
* set up with the current state of a message. The message object is not updated;
* to achieve this, you have to recreate it.
*
@@ -3033,7 +3027,7 @@ dc_msg_t* dc_msg_new (dc_context_t* context, int viewty
/**
* Free a message object. Message objects are created eg. by dc_get_msg().
* Free a message object. Message objects are created e.g. by dc_get_msg().
*
* @memberof dc_msg_t
* @param msg The message object to free.
@@ -3115,12 +3109,12 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
* If a sent message changes to this state, you'll receive the event #DC_EVENT_MSG_READ.
* Also messages already read by some recipients
* may get into the state DC_STATE_OUT_FAILED at a later point,
* eg. when in a group, delivery fails for some recipients.
* e.g. when in a group, delivery fails for some recipients.
*
* If you just want to check if a message is sent or not, please use dc_msg_is_sent() which regards all states accordingly.
*
* The state of just created message objects is DC_STATE_UNDEFINED (0).
* The state is always set by the core-library, users of the library cannot set the state directly, but it is changed implicitly eg.
* The state is always set by the core-library, users of the library cannot set the state directly, but it is changed implicitly e.g.
* when calling dc_marknoticed_chat() or dc_markseen_msgs().
*
* @memberof dc_msg_t
@@ -3134,7 +3128,7 @@ int dc_msg_get_state (const dc_msg_t* msg);
* Get message sending time.
* The sending time is returned as a unix timestamp in seconds.
*
* Note that the message lists returned eg. by dc_get_chat_msgs()
* Note that the message lists returned e.g. by dc_get_chat_msgs()
* are not sorted by the _sending_ time but by the _receiving_ time.
* This ensures newly received messages always pop up at the end of the list,
* however, for delayed messages, the correct sending time will be displayed.
@@ -3166,7 +3160,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg);
/**
* Get message time used for sorting.
* This function returns the timestamp that is used for sorting the message
* into lists as returned eg. by dc_get_chat_msgs().
* into lists as returned e.g. by dc_get_chat_msgs().
* This may be the reveived time, the sending time or another time.
*
* To get the receiving time, use dc_msg_get_received_timestamp().
@@ -3189,7 +3183,7 @@ int64_t dc_msg_get_sort_timestamp (const dc_msg_t* msg);
* it does not make sense to show more text in the message list and typical controls
* will have problems with showing much more text.
* This max. length is to avoid passing _lots_ of data to the frontend which may
* result eg. from decoding errors (assume some bytes missing in a mime structure, forcing
* result e.g. from decoding errors (assume some bytes missing in a mime structure, forcing
* an attachment to be plain text).
*
* To get information about the message and more/raw text, use dc_get_msg_info().
@@ -3247,7 +3241,7 @@ char* dc_msg_get_filemime (const dc_msg_t* msg);
* Get the size of the file. Returns the size of the file associated with a
* message, if applicable.
*
* Typically, this is used to show the size of document messages, eg. a PDF.
* Typically, this is used to show the size of document messages, e.g. a PDF.
*
* @memberof dc_msg_t
* @param msg The message object.
@@ -3446,7 +3440,7 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
* Check if the message is an informational message, created by the
* device or by another users. Such messages are not "typed" by the user but
* created due to other actions,
* eg. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
* or dc_add_contact_to_chat().
*
* These messages are typically shown in the center of the chat view,
@@ -3465,7 +3459,7 @@ int dc_msg_is_info (const dc_msg_t* msg);
/**
* Get the type of an informational message.
* If dc_msg_is_info() returns 1, this function returns the type of the informational message.
* UIs can display eg. an icon based upon the type.
* UIs can display e.g. an icon based upon the type.
*
* Currently, the following types are defined:
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
@@ -3506,7 +3500,7 @@ int dc_msg_is_increation (const dc_msg_t* msg);
/**
* Check if the message is an Autocrypt Setup Message.
*
* Setup messages should be shown in an unique way eg. using a different text color.
* Setup messages should be shown in an unique way e.g. using a different text color.
* On a click or another action, the user should be prompted for the setup code
* which is forwarded to dc_continue_key_transfer() then.
*
@@ -3748,7 +3742,7 @@ char* dc_msg_get_quoted_text (const dc_msg_t* msg);
* To check if a message has a quote, use dc_msg_get_quoted_text().
*
* To display the quote in the chat, use dc_msg_get_quoted_text() as a primary source,
* however, one might add information from the message object (eg. an image).
* however, one might add information from the message object (e.g. an image).
*
* It is not guaranteed that the message belong to the same chat.
*
@@ -3774,7 +3768,7 @@ dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
* authorized-name and given-name.
* By default, these names are equal,
* but functions working with contact names
* (eg. dc_contact_get_name(), dc_contact_get_display_name(),
* (e.g. dc_contact_get_name(), dc_contact_get_display_name(),
* dc_contact_get_name_n_addr(), dc_contact_get_first_name(),
* dc_create_contact() or dc_add_address_book())
* only affect the given-name.
@@ -3791,7 +3785,7 @@ dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
* Free a contact object.
*
* @memberof dc_contact_t
* @param contact The contact object as created eg. by dc_get_contact().
* @param contact The contact object as created e.g. by dc_get_contact().
* If NULL is given, nothing is done.
*/
void dc_contact_unref (dc_contact_t* contact);
@@ -3855,7 +3849,7 @@ char* dc_contact_get_display_name (const dc_contact_t* contact);
* "email@domain.com" if the name is unset.
*
* The summary is typically used when asking the user something about the contact.
* The attached email address makes the question unique, eg. "Chat with Alan Miller (am@uniquedomain.com)?"
* The attached email address makes the question unique, e.g. "Chat with Alan Miller (am@uniquedomain.com)?"
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -3971,9 +3965,9 @@ char* dc_provider_get_overview_page (const dc_provider_t* prov
/**
* Get hints to be shown to the user on the login screen.
* Depending on the @ref DC_PROVIDER_STATUS returned by dc_provider_get_status(),
* the ui may want to highlight the hint.
* the UI may want to highlight the hint.
*
* Moreover, the ui should display a "More information" link
* Moreover, the UI should display a "More information" link
* that forwards to the url returned by dc_provider_get_overview_page().
*
* @memberof dc_provider_t
@@ -4013,7 +4007,7 @@ void dc_provider_unref (dc_provider_t* provider);
* An object containing a set of values.
* The meaning of the values is defined by the function returning the object.
* Lot objects are created
* eg. by dc_chatlist_get_summary() or dc_msg_get_summary().
* e.g. by dc_chatlist_get_summary() or dc_msg_get_summary().
*
* NB: _Lot_ is used in the meaning _heap_ here.
*/
@@ -4027,7 +4021,7 @@ void dc_provider_unref (dc_provider_t* provider);
/**
* Frees an object containing a set of parameters.
* If the set object contains strings, the strings are also freed with this function.
* Set objects are created eg. by dc_chatlist_get_summary() or dc_msg_get_summary().
* Set objects are created e.g. by dc_chatlist_get_summary() or dc_msg_get_summary().
*
* @memberof dc_lot_t
* @param lot The object to free.
@@ -4061,7 +4055,7 @@ char* dc_lot_get_text2 (const dc_lot_t* lot);
/**
* Get the meaning of the first string. Posssible meanings of the string are defined by the creator of the object and may be returned eg.
* Get the meaning of the first string. Posssible meanings of the string are defined by the creator of the object and may be returned e.g.
* as DC_TEXT1_DRAFT, DC_TEXT1_USERNAME or DC_TEXT1_SELF.
*
* @memberof dc_lot_t
@@ -4111,9 +4105,9 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* From the view of the library,
* all types are primary types of the same level,
* eg. the library does not regard #DC_MSG_GIF as a subtype for #DC_MSG_IMAGE
* e.g. the library does not regard #DC_MSG_GIF as a subtype for #DC_MSG_IMAGE
* and it's up to the UI to decide whether a GIF is shown
* eg. in an IMAGE or in a VIDEO container.
* e.g. in an IMAGE or in a VIDEO container.
*
* If you want to define the type of a dc_msg_t object for sending,
* use dc_msg_new().
@@ -4152,7 +4146,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Message containing a sticker, similar to image.
* If possible, the ui should display the image without borders in a transparent way.
* If possible, the UI should display the image without borders in a transparent way.
* A click on a sticker will offer to install the sticker set in some future.
*/
#define DC_MSG_STICKER 23
@@ -4187,7 +4181,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Message containing any file, eg. a PDF.
* Message containing any file, e.g. a PDF.
* The file is set via dc_msg_set_file()
* and retrieved via dc_msg_get_file().
*/
@@ -4199,7 +4193,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* The message was created via dc_send_videochat_invitation() on this or a remote device.
*
* Typically, such messages are rendered differently by the UIs,
* eg. contain a button to join the videochat.
* e.g. contain a button to join the videochat.
* The url for joining can be retrieved using dc_msg_get_videochat_url().
*/
#define DC_MSG_VIDEOCHAT_INVITATION 70
@@ -4333,7 +4327,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @return An event as an dc_event_t object.
* You can query the event for information using dc_event_get_id(), dc_event_get_data1_int() and so on;
* if you are done with the event, you have to free the event using dc_event_unref().
* If NULL is returned, the context belonging to the event emitter is unref'd and the no more events will come;
* If NULL is returned, the context belonging to the event emitter is unref'd and no more events will come;
* in this case, free the event emitter using dc_event_emitter_unref().
*/
dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
@@ -4366,7 +4360,7 @@ void dc_event_emitter_unref(dc_event_emitter_t* emitter);
* @return An event as an dc_event_t object.
* You can query the event for information using dc_event_get_id(), dc_event_get_data1_int() and so on;
* if you are done with the event, you have to free the event using dc_event_unref().
* If NULL is returned, the context belonging to the event emitter is unref'd and the no more events will come;
* If NULL is returned, the contexts belonging to the event emitter are unref'd and no more events will come;
* in this case, free the event emitter using dc_accounts_event_emitter_unref().
*/
dc_event_t* dc_accounts_get_next_event (dc_accounts_event_emitter_t* emitter);
@@ -4488,7 +4482,7 @@ void dc_event_unref(dc_event_t* event);
* This event should not be reported to the end-user using a popup or something like that.
*
* @param data1 0
* @param data2 (char*) Info string in english language.
* @param data2 (char*) Info string in English language.
*/
#define DC_EVENT_INFO 100
@@ -4497,7 +4491,7 @@ void dc_event_unref(dc_event_t* event);
* Emitted when SMTP connection is established and login was successful.
*
* @param data1 0
* @param data2 (char*) Info string in english language.
* @param data2 (char*) Info string in English language.
*/
#define DC_EVENT_SMTP_CONNECTED 101
@@ -4506,7 +4500,7 @@ void dc_event_unref(dc_event_t* event);
* Emitted when IMAP connection is established and login was successful.
*
* @param data1 0
* @param data2 (char*) Info string in english language.
* @param data2 (char*) Info string in English language.
*/
#define DC_EVENT_IMAP_CONNECTED 102
@@ -4514,7 +4508,7 @@ void dc_event_unref(dc_event_t* event);
* Emitted when a message was successfully sent to the SMTP server.
*
* @param data1 0
* @param data2 (char*) Info string in english language.
* @param data2 (char*) Info string in English language.
*/
#define DC_EVENT_SMTP_MESSAGE_SENT 103
@@ -4522,7 +4516,7 @@ void dc_event_unref(dc_event_t* event);
* Emitted when a message was successfully marked as deleted on the IMAP server.
*
* @param data1 0
* @param data2 (char*) Info string in english language.
* @param data2 (char*) Info string in English language.
*/
#define DC_EVENT_IMAP_MESSAGE_DELETED 104
@@ -4530,7 +4524,7 @@ void dc_event_unref(dc_event_t* event);
* Emitted when a message was successfully moved on IMAP.
*
* @param data1 0
* @param data2 (char*) Info string in english language.
* @param data2 (char*) Info string in English language.
*/
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
@@ -4556,7 +4550,7 @@ void dc_event_unref(dc_event_t* event);
* This event should not be reported to the end-user using a popup or something like that.
*
* @param data1 0
* @param data2 (char*) Warning string in english language.
* @param data2 (char*) Warning string in English language.
*/
#define DC_EVENT_WARNING 300
@@ -4567,16 +4561,16 @@ void dc_event_unref(dc_event_t* event);
* As most things are asynchronous, things may go wrong at any time and the user
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
*
* However, for ongoing processes (eg. dc_configure())
* or for functions that are expected to fail (eg. dc_continue_key_transfer())
* However, for ongoing processes (e.g. dc_configure())
* or for functions that are expected to fail (e.g. dc_continue_key_transfer())
* it might be better to delay showing these events until the function has really
* failed (returned false). It should be sufficient to report only the _last_ error
* in a messasge box then.
* in a message box then.
*
* @param data1 0
* @param data2 (char*) Error string, always set, never NULL.
* 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.
*/
#define DC_EVENT_ERROR 400
@@ -4603,13 +4597,13 @@ void dc_event_unref(dc_event_t* event);
/**
* An action cannot be performed because the user is not in the group.
* Reported eg. after a call to
* Reported e.g. after a call to
* dc_set_chat_name(), dc_set_chat_profile_image(),
* dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
* dc_send_text_msg() or another sending function.
*
* @param data1 0
* @param data2 (char*) Info string in english language.
* @param data2 (char*) Info string in English language.
*/
#define DC_EVENT_ERROR_SELF_NOT_IN_GROUP 410
@@ -4641,12 +4635,12 @@ void dc_event_unref(dc_event_t* event);
/**
* Messages were marked noticed or seen.
* The ui may update badge counters or stop showing a chatlist-item with a bold font.
* The UI may update badge counters or stop showing a chatlist-item with a bold font.
*
* This event is emitted eg. when calling dc_markseen_msgs(), dc_marknoticed_chat() or dc_marknoticed_contact()
* This event is emitted e.g. when calling dc_markseen_msgs(), dc_marknoticed_chat() or dc_marknoticed_contact()
* or when a chat is answered on another device.
* Do not try to derive the state of an item from just the fact you received the event;
* use eg. dc_msg_get_state() or dc_get_fresh_msg_cnt() for this purpose.
* use e.g. dc_msg_get_state() or dc_get_fresh_msg_cnt() for this purpose.
*
* @param data1 (int) chat_id
* @param data2 0
@@ -4717,7 +4711,7 @@ void dc_event_unref(dc_event_t* event);
*
* @param data1 (int) contact_id of the contact for which the location has changed.
* If the locations of several contacts have been changed,
* eg. after calling dc_delete_all_locations(), this parameter is set to 0.
* e.g. after calling dc_delete_all_locations(), this parameter is set to 0.
* @param data2 0
*/
#define DC_EVENT_LOCATION_CHANGED 2035
@@ -4832,7 +4826,7 @@ void dc_event_unref(dc_event_t* event);
* works by just entering the name or the email-address.
*
* - There is no need for the user to do any special things
* (enable IMAP or so) in the provider's webinterface or at other places.
* (enable IMAP or so) in the provider's web interface or at other places.
* - There is no need for the user to enter advanced settings;
* server, port etc. are known by the core.
*
@@ -4843,8 +4837,8 @@ void dc_event_unref(dc_event_t* event);
/**
* Provider works, but there are preparations needed.
*
* - The user has to do some special things as "Enable IMAP in the Webinterface",
* what exactly, is described in the string returnd by dc_provider_get_before_login_hints()
* - The user has to do some special things as "Enable IMAP in the web interface",
* what exactly, is described in the string returned by dc_provider_get_before_login_hints()
* and, typically more detailed, in the page linked by dc_provider_get_overview_page().
* - There is no need for the user to enter advanced settings;
* server, port etc. should be known by the core.
@@ -4857,7 +4851,7 @@ void dc_event_unref(dc_event_t* event);
* Provider is not working.
* This provider status is returned for providers
* that are known to not work with Delta Chat.
* The ui should block logging in with this provider.
* The UI should block logging in with this provider.
*
* More information about that is typically provided
* in the string returned by dc_provider_get_before_login_hints()
@@ -4892,10 +4886,10 @@ void dc_event_unref(dc_event_t* event);
/**
* Archived chats are not included in the default chatlist returned by dc_get_chatlist().
* Instead, if there are _any_ archived chats, the pseudo-chat
* with the chat_id DC_CHAT_ID_ARCHIVED_LINK will be added the the end of the chatlist.
* with the chat_id DC_CHAT_ID_ARCHIVED_LINK will be added at the end of the chatlist.
*
* The UI typically shows a little icon or chats beside archived chats in the chatlist,
* this is needed as eg. the search will also return archived chats.
* this is needed as e.g. the search will also return archived chats.
*
* If archived chats receive new messages, they become normal chats again.
*
@@ -4915,7 +4909,7 @@ void dc_event_unref(dc_event_t* event);
/*
* TODO: Strings need some doumentation about used placeholders.
* TODO: Strings need some documentation about used placeholders.
*
* @defgroup DC_STR DC_STR
*
@@ -4987,7 +4981,8 @@ void dc_event_unref(dc_event_t* event);
#define DC_STR_ERROR_NO_NETWORK 87
#define DC_STR_PROTECTION_ENABLED 88
#define DC_STR_PROTECTION_DISABLED 89
#define DC_STR_REPLY_NOUN 90 /* eg. "Reply", used in summaries, a noun, not a verb (not: "to reply") */
#define DC_STR_REPLY_NOUN 90 /* e.g. "Reply", used in summaries, a noun, not a verb (not: "to reply") */
#define DC_STR_SELF_DELETED_MSG_BODY 91
/*
* @}

View File

@@ -1727,13 +1727,15 @@ pub unsafe extern "C" fn dc_imex(
let ctx = &*context;
let param1 = to_opt_string_lossy(param1);
spawn(async move {
imex::imex(&ctx, what, param1)
.await
.log_err(ctx, "IMEX failed")
});
if let Some(param1) = to_opt_string_lossy(param1) {
spawn(async move {
imex::imex(&ctx, what, &param1)
.await
.log_err(ctx, "IMEX failed")
});
} else {
eprintln!("dc_imex called without a valid directory");
}
}
#[no_mangle]
@@ -3006,6 +3008,11 @@ pub unsafe extern "C" fn dc_msg_set_quote(msg: *mut dc_msg_t, quote: *const dc_m
let ffi_msg = &mut *msg;
let ffi_quote = &*quote;
if ffi_msg.context != ffi_quote.context {
eprintln!("ignoring attempt to quote message from a different context");
return;
}
block_on(async move {
ffi_msg
.message

View File

@@ -5,69 +5,95 @@ Problem: missing eventual group consistency
If group members are concurrently adding new members,
the new members will miss each other's additions, example:
- Alice and Bob are in a two-member group
1. Alice and Bob are in a two-member group
- Alice adds Carol, concurrently Bob adds Doris
2. Then Alice adds Carol, while concurrently Bob adds Doris
- Carol will see a three-member group (Alice, Bob, Carol),
Doris will see a different three-member group (Alice, Bob, Doris),
and only Alice and Bob will have all four members.
Right now, the group has inconsistent memberships:
Note that for verified groups any mitigation mechanism likely
needs to make all clients to know who originally added a member.
- Alice and Carol see a (Alice, Carol, Bob) group
- Bob and Doris see a (Bob, Doris, Alice)
This then leads to "sender is unknown" messages in the chat,
for example when Alice receives a message from Doris,
or when Bob receives a message from Carol.
There are also other sources for group membership inconsistency:
- leaving/deleting/adding in larger groups, while being offline,
increases chances for inconsistent group membership
- dropped group-membership messages
- group-membership messages landing in "Spam"
solution: memorize+attach (possible encrypted) chat-meta mime messages
----------------------------------------------------------------------
Note that all these problems (can) also happen with verified groups,
then raising "false alarms" which could lure people to ignore such issues.
For reference, please see https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members how MemberAdded/Removed messages are shaped.
IOW, it's clear we need to do something about it to improve overall
reliability in group-settings.
- All Chat-Group-Member-Added/Removed messages are recorded in their
full raw (signed and encrypted) mime-format in the DB
- If an incoming member-add/member-delete messages has a member list
which is, apart from the added/removed member, not consistent
with our own view, broadcast a "Chat-Group-Member-Correction" message to
all members, attaching the original added/removed mime-message for all mismatching
contacts. If we have no relevant add/del information, don't send a
correction message out.
Solution: replay group modification messages on inconsistencies
------------------------------------------------------------------
- Upong receiving added/removed attachments we don't do the
check_consistency+correction message dance.
This avoids recursion problems and hard-to-reason-about chatter.
For brevity let's abbreviate "group membership modification" as **GMM**.
Notes:
Delta chat has explicit GMM messages, typically encrypted to the group members
as seen by the device that sends the GMM. The `Spec <https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members>`_ details the Mime headers and format.
- mechanism works for both encrypted and unencrypted add/del messages
If we detect membership inconsistencies we can resend relevant GMM messages
to the respective chat. The receiving devices can process those GMM messages
as if it would be an incoming message. If for example they have already seen
the Message-ID of the GMM message, they will ignore the message. It's
probably useful to record GMM message in their original MIME-format and
not invent a new recording format. Few notes on three aspects:
- we already have a "mime_headers" column in the DB for each incoming message.
We could extend it to also include the payload and store mime unconditionally
for member-added/removed messages.
- **group-membership-tracking**: All valid GMM messages are persisted in
their full raw (signed/encrypted?) MIME-format in the DB. Note that GMM messages
already are in the msgs table, and there is a mime_header column which we could
extend to contain the raw Mime GMM message.
- multiple member-added/removed messages can be attached in a single
correction message
- **consistency_checking**: If an incoming GMM has a member list which is
not consistent with our own view, broadcast a "Group-Member-Correction"
message to all members containing a multipart list of GMMs.
- it is minimal on the number of overall messages to reach group consistency
(best-case: no extra messages, the ABCD case above: max two extra messages)
- **correcting_memberships**: Upon receiving a Group-Member-Correction
message we pass the contained GMMs to the "incoming mail pipeline"
(without **consistency_checking** them, to avoid recursion issues)
- somewhat backward compatible: older clients will probably ignore
messages which are signed by someone who is not the outer From-address.
- the correction-protocol also helps with dropped messages. If a member
did not see a member-added/removed message, the next member add/removed
message in the group will likely heal group consistency for this member.
Alice/Carol and Bob/Doris getting on the same page
++++++++++++++++++++++++++++++++++++++++++++++++++
Recall that Alice/Carol and Bob/Doris had a differening view of
group membership. With the proposed solution, when Bob receives
Alice's "Carol added" message, he will notice that Alice (and thus
also carol) did not know about Doris. Bob's device sends a
"Chat-Group-Member-Correction" message containing his own GMM
when adding Doris. Therefore, the group's membership is healed
for everyone in a single broadcast message.
Alice might also send a Group-member-Correction message,
so there is a second chance that the group gets to know all GMMs.
Note, for example, that if for some reason Bobs and Carols provider
drop GMM messages between them (spam) that Alice and Doris can heal
it by resending GMM messages whenever they detect them to be out of sync.
- we can quite easily extend the mechanism to also provide the group-avatar or
other meta-information.
Discussions of variants
++++++++++++++++++++++++
- instead of acting on MemberAdded/Removed message we could send
corrections for any received message that addresses inconsistent group members but
a) this would delay group-membership healing
- instead of acting on GMM messages we could send corrections
for any received message that addresses inconsistent group members but
a) this could delay group-membership healing
b) could lead to a lot of members sending corrections
c) means we might rely on "To-Addresses" which we also like to strike
at least for protected chats.
- instead of broadcasting correction messages we could only send it to
the sender of the inconsistent member-added/removed message.
@@ -83,44 +109,3 @@ Discussions of variants
while both being in an offline or bad-connection situation).
solution2: repeat member-added/removed messages
---------------------------------------------------
Introduce a new Chat-Group-Member-Changed header and deprecate Chat-Group-Member-Added/Removed
but keep sending out the old headers until the new protocol is sufficiently deployed.
The new Chat-Group-Member-Changed header contains a Time-to-Live number (TTL)
which controls repetition of the signed "add/del e-mail address" payload.
Example::
Chat-Group-Member-Changed: TTL add "somedisplayname" someone@example.org
owEBYQGe/pANAwACAY47A6J5t3LWAcsxYgBeTQypYWRkICJzb21lZGlzcGxheW5h
bWUiIHNvbWVvbmVAZXhhbXBsZS5vcmcgCokBHAQAAQIABgUCXk0MqQAKCRCOOwOi
ebdy1hfRB/wJ74tgFQulicthcv9n+ZsqzwOtBKMEVIHqJCzzDB/Hg/2z8ogYoZNR
iUKKrv3Y1XuFvdKyOC+wC/unXAWKFHYzY6Tv6qDp6r+amt+ad+8Z02q53h9E55IP
FUBdq2rbS8hLGjQB+mVRowYrUACrOqGgNbXMZjQfuV7fSc7y813OsCQgi3tjstup
b+uduVzxCp3PChGhcZPs3iOGCnQvSB8VAaLGMWE2d7nTo/yMQ0Jx69x5qwfXogTk
mTt5rOJyrosbtf09TMKFzGgtqBcEqHLp3+mQpZQ+WHUKAbsRa8Jc9DOUOSKJ8SNM
clKdskprY+4LY0EBwLD3SQ7dPkTITCRD
=P6GG
TTL is set to "2" on an initial Chat-Group-Member-Changed add/del message.
Receivers will apply the add/del change to the group-membership,
decrease the TTL by 1, and if TTL>0 re-sent the header.
The "add|del e-mail address" payload is pgp-signed and repeated verbatim.
This allows to propagate, in a cryptographically secured way,
who added a member. This is particularly important for allowing
to show in verified groups who added a member (planned).
Disadvantage to solution 1:
- requires to specify encoding and precise rules for what/how is signed.
- causes O(N^2) extra messages
- Not easily extendable for other things (without introducing a new
header / encoding)

View File

@@ -443,20 +443,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"export-backup" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportBackup, Some(&dir)).await?;
imex(&context, ImexMode::ExportBackup, &dir).await?;
println!("Exported to {}.", dir.to_string_lossy());
}
"import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
imex(&context, ImexMode::ImportBackup, Some(arg1)).await?;
imex(&context, ImexMode::ImportBackup, arg1).await?;
}
"export-keys" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportSelfKeys, Some(&dir)).await?;
imex(&context, ImexMode::ExportSelfKeys, &dir).await?;
println!("Exported to {}.", dir.to_string_lossy());
}
"import-keys" => {
imex(&context, ImexMode::ImportSelfKeys, Some(arg1)).await?;
imex(&context, ImexMode::ImportSelfKeys, arg1).await?;
}
"export-setup" => {
let setup_code = create_setup_code(&context);

View File

@@ -197,14 +197,12 @@ const CHAT_COMMANDS: [&str; 27] = [
"unpin",
"delchat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
const MESSAGE_COMMANDS: [&str; 6] = [
"listmsgs",
"msginfo",
"listfresh",
"forward",
"markseen",
"star",
"unstar",
"delmsg",
];
const CONTACT_COMMANDS: [&str; 6] = [

View File

@@ -214,6 +214,19 @@ class Account(object):
:param name: (optional) display name for this contact
:returns: :class:`deltachat.contact.Contact` instance.
"""
(name, addr) = self.get_contact_addr_and_name(obj, name)
name = as_dc_charpointer(name)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
return Contact(self, contact_id)
def get_contact(self, obj):
if isinstance(obj, Contact):
return obj
(_, addr) = self.get_contact_addr_and_name(obj)
return self.get_contact_by_addr(addr)
def get_contact_addr_and_name(self, obj, name=None):
if isinstance(obj, Account):
if not obj.is_configured():
raise ValueError("can only add addresses from configured accounts")
@@ -229,13 +242,7 @@ class Account(object):
if name is None and displayname:
name = displayname
return self._create_contact(addr, name)
def _create_contact(self, addr, name):
addr = as_dc_charpointer(addr)
name = as_dc_charpointer(name)
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
return Contact(self, contact_id)
return (name, addr)
def delete_contact(self, contact):
""" delete a Contact.
@@ -263,6 +270,17 @@ class Account(object):
"""
return Contact(self, contact_id)
def get_blocked_contacts(self):
""" return a list of all blocked contacts.
:returns: list of :class:`deltachat.contact.Contact` objects.
"""
dc_array = ffi.gc(
lib.dc_get_blocked_contacts(self._dc_context),
lib.dc_array_unref
)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_contacts(self, query=None, with_self=False, only_verified=False):
""" get a (filtered) list of contacts.

View File

@@ -371,7 +371,7 @@ class Chat(object):
:raises ValueError: if contact could not be removed
:returns: None
"""
contact = self.account.create_contact(obj)
contact = self.account.get_contact(obj)
ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError("could not remove contact {!r} from chat".format(contact))

View File

@@ -52,9 +52,17 @@ class Contact(object):
return lib.dc_contact_is_blocked(self._dc_contact)
def set_blocked(self, block=True):
""" Block or unblock a contact. """
""" [Deprecated, use block/unblock methods] Block or unblock a contact. """
return lib.dc_block_contact(self.account._dc_context, self.id, block)
def block(self):
""" Block this contact. Message will not be seen/retrieved from this contact. """
return lib.dc_block_contact(self.account._dc_context, self.id, True)
def unblock(self):
""" Unblock this contact. Messages from this contact will be retrieved (again)."""
return lib.dc_block_contact(self.account._dc_context, self.id, False)
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)

View File

@@ -103,6 +103,14 @@ class FFIEventTracker:
if rex.search(ev.data2):
return ev
def get_info_regex_groups(self, regex, check_error=True):
rex = re.compile(regex)
while 1:
ev = self.get_matching("DC_EVENT_INFO", check_error=check_error)
m = rex.match(ev.data2)
if m is not None:
return m.groups()
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile("(?:{}).*".format(event_name_regex))

View File

@@ -330,6 +330,13 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
return accounts
def clone_online_account(self, account, pre_generated_key=True):
""" Clones addr, mail_pw, mvbox_watch, mvbox_move, sentbox_watch and the
direct_imap object of an online account. This simulates the user setting
up a new device without importing a backup.
`pre_generated_key` only means that a key from python/tests/data/key is
used in order to speed things up.
"""
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
@@ -503,6 +510,9 @@ def lp():
def step(self, msg):
print("-" * 5, "step " + msg, "-" * 5)
def indent(self, msg):
print(" " + msg)
return Printer()

View File

@@ -129,6 +129,20 @@ class TestOfflineContact:
assert not contact1.is_blocked()
assert not contact1.is_verified()
def test_get_blocked(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
contact2 = ac1.create_contact("some2@example.org", name="some2")
ac1.create_contact("some3@example.org", name="some3")
assert ac1.get_blocked_contacts() == []
contact1.block()
assert ac1.get_blocked_contacts() == [contact1]
contact2.block()
blocked = ac1.get_blocked_contacts()
assert len(blocked) == 2 and contact1 in blocked and contact2 in blocked
contact2.unblock()
assert ac1.get_blocked_contacts() == [contact1]
def test_create_self_contact(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
contact1 = ac1.create_contact(ac1.get_config("addr"))
@@ -274,6 +288,28 @@ class TestOfflineChat:
qr = chat.get_join_qr()
assert ac2.check_qr(qr).is_ask_verifygroup
def test_removing_blocked_user_from_group(self, ac1, lp):
"""
Test that blocked contact is not unblocked when removed from a group.
See https://github.com/deltachat/deltachat-core-rust/issues/2030
"""
lp.sec("Create a group chat with a contact")
contact = ac1.create_contact("some1@example.org")
group = ac1.create_group_chat("title", contacts=[contact])
group.send_text("First group message")
lp.sec("ac1 blocks contact")
contact.block()
assert contact.is_blocked()
lp.sec("ac1 removes contact from their group")
group.remove_contact(contact)
assert contact.is_blocked()
lp.sec("ac1 adding blocked contact unblocks it")
group.add_contact(contact)
assert not contact.is_blocked()
def test_get_set_profile_image_simple(self, ac1, data):
chat = ac1.create_group_chat(name="title1")
p = data.get_path("d.png")
@@ -632,7 +668,7 @@ class TestOnlineAccount:
except Exception:
pass
def test_export_import_self_keys(self, acfactory, tmpdir):
def test_export_import_self_keys(self, acfactory, tmpdir, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
dir = tmpdir.mkdir("exportdir")
@@ -640,8 +676,17 @@ class TestOnlineAccount:
assert len(export_files) == 2
for x in export_files:
assert x.startswith(dir.strpath)
key_id, = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
ac1._evtracker.consume_events()
lp.sec("exported keys (private and public)")
for name in os.listdir(dir.strpath):
lp.indent(dir.strpath + os.sep + name)
lp.sec("importing into existing account")
ac2.import_self_keys(dir.strpath)
key_id2, = ac2._evtracker.get_info_regex_groups(
r".*stored.*KeyId\((.*)\).*", check_error=False)
assert key_id2 == key_id
def test_one_account_send_bcc_setting(self, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
@@ -924,6 +969,30 @@ class TestOnlineAccount:
except queue.Empty:
pass # mark_seen_messages() has generated events before it returns
def test_reply_privately(self, acfactory):
ac1, ac2 = acfactory.get_two_online_accounts()
group1 = ac1.create_group_chat("group")
group1.add_contact(ac2)
group1.send_text("hello")
msg2 = ac2._evtracker.wait_next_messages_changed()
group2 = msg2.create_chat()
assert group2.get_name() == group1.get_name()
msg_reply = Message.new_empty(ac2, "text")
msg_reply.set_text("message reply")
msg_reply.quote = msg2
private_chat1 = ac1.create_chat(ac2)
private_chat2 = ac2.create_chat(ac1)
private_chat2.send_msg(msg_reply)
msg_reply1 = ac1._evtracker.wait_next_incoming_message()
assert msg_reply1.quoted_text == "hello"
assert not msg_reply1.chat.is_group()
assert msg_reply1.chat.id == private_chat1.id
def test_mdn_asymetric(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts(move=True)
@@ -1092,8 +1161,8 @@ class TestOnlineAccount:
# Majority prefers encryption now
assert msg5.is_encrypted()
def test_reply_encrypted(self, acfactory, lp):
"""Test that replies to encrypted messages are encrypted."""
def test_quote_encrypted(self, acfactory, lp):
"""Test that replies to encrypted messages with quotes are encrypted."""
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")
@@ -1142,6 +1211,39 @@ class TestOnlineAccount:
assert msg_in.quoted_text == quoted_msg.text
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
def test_quote_attachment(self, tmpdir, acfactory, lp):
"""Test that replies with an attachment and a quote are received correctly."""
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1 creates chat with ac2")
chat1 = ac1.create_chat(ac2)
lp.sec("ac1 sends text message to ac2")
chat1.send_text("hi")
lp.sec("ac2 receives contact request from ac1")
received_message = ac2._evtracker.wait_next_messages_changed()
assert received_message.text == "hi"
basename = "attachment.txt"
p = os.path.join(tmpdir.strpath, basename)
with open(p, "w") as f:
f.write("data to send")
lp.sec("ac2 sends a reply to ac1")
chat2 = received_message.create_chat()
reply = Message.new_empty(ac2, "file")
reply.set_text("message reply")
reply.set_file(p)
reply.quote = received_message
chat2.send_msg(reply)
lp.sec("ac1 receives a reply from ac2")
received_reply = ac1._evtracker.wait_next_incoming_message()
assert received_reply.text == "message reply"
assert received_reply.quoted_text == received_message.text
assert open(received_reply.filename).read() == "data to send"
def test_saved_mime_on_received_message(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1507,7 +1609,7 @@ class TestOnlineAccount:
lp.sec("ac1 blocks ac2")
contact = ac1.create_contact(ac2)
contact.set_blocked()
contact.block()
assert contact.is_blocked()
ev = ac1._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
assert ev.data1 == contact.id
@@ -1961,6 +2063,7 @@ class TestOnlineAccount:
assert ac1.direct_imap.idle_wait_for_seen()
ac1_clone = acfactory.clone_online_account(ac1)
ac1_clone.set_config("fetch_existing_msgs", "1")
ac1_clone._configtracker.wait_finish()
ac1_clone.start_io()

View File

@@ -63,13 +63,14 @@ def main():
ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml")
assert core_toml == ffi_toml, (core_toml, ffi_toml)
for line in open("CHANGELOG.md"):
## 1.25.0
if line.startswith("## "):
if line[2:].strip().startswith(newversion):
break
else:
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
if "alpha" not in newversion:
for line in open("CHANGELOG.md"):
## 1.25.0
if line.startswith("## "):
if line[2:].strip().startswith(newversion):
break
else:
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
replace_toml_version("Cargo.toml", newversion)
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)

View File

@@ -188,7 +188,7 @@ impl Accounts {
let id = self.add_account().await?;
let ctx = self.get_account(id).await.expect("just added");
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, Some(file)).await {
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, &file).await {
Ok(_) => Ok(id),
Err(err) => {
// remove temp account
@@ -347,7 +347,6 @@ impl Config {
inner.accounts.push(AccountConfig {
id,
name: String::new(),
dir: target_dir.into(),
uuid,
});
@@ -414,8 +413,6 @@ impl Config {
pub struct AccountConfig {
/// Unique id.
pub id: u32,
/// Display name
pub name: String,
/// Root directory for all data for this account.
pub dir: std::path::PathBuf,
pub uuid: Uuid,

View File

@@ -63,6 +63,12 @@ impl<'a> BlobObject<'a> {
blobname: name.clone(),
cause: err.into(),
})?;
// workaround a bug in async-std
// (the executor does not handle blocking operation in Drop correctly,
// see https://github.com/async-rs/async-std/issues/900 )
let _ = file.flush().await;
let blob = BlobObject {
blobdir,
name: format!("$BLOBDIR/{}", name),
@@ -151,6 +157,10 @@ impl<'a> BlobObject<'a> {
cause: err,
});
}
// workaround, see create() for details
let _ = dst_file.flush().await;
let blob = BlobObject {
blobdir: context.get_blobdir(),
name: format!("$BLOBDIR/{}", name),

View File

@@ -337,7 +337,7 @@ impl ChatId {
);
/* Up to 2017-11-02 deleting a group also implied leaving it, see above why we have changed this. */
let _chat = Chat::load_from_db(context, self).await?;
let chat = Chat::load_from_db(context, self).await?;
context
.sql
.execute(
@@ -373,6 +373,17 @@ impl ChatId {
let j = job::Job::new(Action::Housekeeping, 0, Params::new(), 10);
job::add(context, j).await;
if chat.is_self_talk() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(
context
.stock_str(StockMessage::SelfDeletedMsgBody)
.await
.into(),
);
add_device_msg(&context, None, Some(&mut msg)).await?;
}
Ok(())
}

View File

@@ -69,8 +69,12 @@ pub enum Config {
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
MediaQuality,
#[strum(props(default = "1"))]
FetchExisting,
/// If set to "1", on the first time `start_io()` is called after configuring,
/// the newest existing messages are fetched.
/// Existing recipients are added to the contact database regardless of this setting.
#[strum(props(default = "0"))]
// disabled for now, we'll set this back to "1" at some point
FetchExistingMsgs,
#[strum(props(default = "0"))]
KeyGenType,
@@ -249,6 +253,16 @@ impl Context {
job::schedule_resync(self).await;
ret
}
Config::InboxWatch => {
if self.get_config(Config::InboxWatch).await.as_deref() != value {
// If Inbox-watch is disabled and enabled again, do not fetch emails from in between.
// this avoids unexpected mass-downloads and -deletions (if delete_server_after is set)
if let Some(inbox) = self.get_config(Config::ConfiguredInboxFolder).await {
crate::imap::set_config_last_seen_uid(self, inbox, 0, 0).await;
}
}
self.sql.set_raw_config(self, key, value).await
}
_ => self.sql.set_raw_config(self, key, value).await,
}
}

View File

@@ -217,28 +217,32 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 500);
let servers = expand_param_vector(
param_autoconfig.unwrap_or_else(|| {
vec![
ServerParams {
protocol: Protocol::IMAP,
hostname: param.imap.server.clone(),
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
},
ServerParams {
protocol: Protocol::SMTP,
hostname: param.smtp.server.clone(),
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
},
]
}),
&param.addr,
&param_domain,
);
let mut servers = param_autoconfig.unwrap_or_default();
if !servers
.iter()
.any(|server| server.protocol == Protocol::IMAP)
{
servers.push(ServerParams {
protocol: Protocol::IMAP,
hostname: param.imap.server.clone(),
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
})
}
if !servers
.iter()
.any(|server| server.protocol == Protocol::SMTP)
{
servers.push(ServerParams {
protocol: Protocol::SMTP,
hostname: param.smtp.server.clone(),
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
})
}
let servers = expand_param_vector(servers, &param.addr, &param_domain);
progress!(ctx, 550);
@@ -560,7 +564,9 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
let first_err = if let Some(f) = errors.first() {
f
} else {
return "".to_string();
// This means configuration failed but no errors have been captured. This should never
// happen, but if it does, the user will see classic "Error: no error".
return "no error".to_string();
};
if errors

View File

@@ -205,6 +205,11 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is splitted to chunks.
// this does not affect MIME'e `To:` header.
// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db.
pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
#[derive(
Debug,
Display,

View File

@@ -163,10 +163,6 @@ impl Context {
/// Stops the IO scheduler.
pub async fn stop_io(&self) {
info!(self, "stopping IO");
if !self.is_io_running().await {
info!(self, "IO is not running");
return;
}
self.inner.stop_io().await;
}
@@ -483,14 +479,19 @@ impl InnerContext {
}
async fn stop_io(&self) {
assert!(self.is_io_running().await, "context is already stopped");
let token = {
let lock = &*self.scheduler.read().await;
lock.pre_stop().await
};
{
let lock = &mut *self.scheduler.write().await;
lock.stop(token).await;
if self.is_io_running().await {
let token = {
let lock = &*self.scheduler.read().await;
lock.pre_stop().await
};
{
let lock = &mut *self.scheduler.write().await;
lock.stop(token).await;
}
}
if let Some(ephemeral_task) = self.ephemeral_task.write().await.take() {
ephemeral_task.cancel().await;
}
}
}

View File

@@ -387,6 +387,7 @@ async fn add_parts(
// this message is a classic email not a chat-message nor a reply to one
match show_emails {
ShowEmails::Off => {
info!(context, "Classical email not shown (TRASH)");
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
allow_creation = false;
}
@@ -445,10 +446,7 @@ async fn add_parts(
// it might also be blocked and displayed in the deaddrop as a result
if chat_id.is_unset() && mime_parser.failure_report.is_some() {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
info!(
context,
"Message belongs to an NDN and is not shown in a chat.",
);
info!(context, "Message belongs to an NDN (TRASH)",);
}
if chat_id.is_unset() {
@@ -489,7 +487,7 @@ async fn add_parts(
// check if the message belongs to a mailing list
if mime_parser.is_mailinglist_message() {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
info!(context, "Message belongs to a mailing list and is ignored.",);
info!(context, "Message belongs to a mailing list (TRASH)");
}
}
@@ -533,6 +531,7 @@ async fn add_parts(
if chat_id.is_unset() {
// maybe from_id is null or sth. else is suspicious, move message to trash
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
info!(context, "No chat id for incoming msg (TRASH)")
}
// if the chat_id is blocked,
@@ -642,13 +641,14 @@ async fn add_parts(
}
if chat_id.is_unset() {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
info!(context, "No chat id for outgoing message (TRASH)")
}
}
if fetching_existing_messages && mime_parser.decrypting_failed {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
// We are only gathering old messages on first start. We do not want to add loads of non-decryptable messages to the chats.
info!(context, "Dropping existing non-decipherable message.");
info!(context, "Existing non-decipherable message. (TRASH)");
}
// Extract ephemeral timer from the message.
@@ -772,9 +772,6 @@ async fn add_parts(
*sent_timestamp = std::cmp::min(*sent_timestamp, rcvd_timestamp);
// unarchive chat
chat_id.unarchive(context).await?;
// if the mime-headers should be saved, find out its size
// (the mime-header ends with an empty line)
let save_mime_headers = context.get_config_bool(Config::SaveMimeHeaders).await;
@@ -896,6 +893,10 @@ async fn add_parts(
*insert_msg_id = *id;
}
if !is_hidden {
chat_id.unarchive(context).await?;
}
*hidden = is_hidden;
created_db_entries.extend(ids.iter().map(|id| (chat_id, *id)));
mime_parser.parts = new_parts;
@@ -1079,37 +1080,24 @@ async fn create_or_lookup_group(
set_better_msg(mime_parser, &better_msg);
}
let mut grpid = "".to_string();
if let Some(optional_field) = mime_parser.get(HeaderDef::ChatGroupId) {
grpid = optional_field.clone();
}
let grpid = try_getting_grpid(mime_parser);
if grpid.is_empty() {
if let Some(extracted_grpid) = mime_parser
.get(HeaderDef::MessageId)
.and_then(|value| dc_extract_grpid_from_rfc724_mid(&value))
{
grpid = extracted_grpid.to_string();
} else if let Some(extracted_grpid) = extract_grpid(mime_parser, HeaderDef::InReplyTo) {
grpid = extracted_grpid.to_string();
} else if let Some(extracted_grpid) = extract_grpid(mime_parser, HeaderDef::References) {
grpid = extracted_grpid.to_string();
} else {
return create_or_lookup_adhoc_group(
context,
mime_parser,
allow_creation,
create_blocked,
from_id,
to_ids,
)
.await
.map_err(|err| {
info!(context, "could not create adhoc-group: {:?}", err);
err
});
}
return create_or_lookup_adhoc_group(
context,
mime_parser,
allow_creation,
create_blocked,
from_id,
to_ids,
)
.await
.map_err(|err| {
info!(context, "could not create adhoc-group: {:?}", err);
err
});
}
// now we have a grpid that is non-empty
// but we might not know about this group
@@ -1279,6 +1267,7 @@ async fn create_or_lookup_group(
} else {
// The message was decrypted successfully, but contains a late "quit" or otherwise
// unwanted message.
info!(context, "message belongs to unwanted group (TRASH)");
return Ok((ChatId::new(DC_CHAT_ID_TRASH), chat_id_blocked));
}
}
@@ -1378,6 +1367,27 @@ async fn create_or_lookup_group(
Ok((chat_id, chat_id_blocked))
}
fn try_getting_grpid(mime_parser: &MimeMessage) -> String {
if let Some(optional_field) = mime_parser.get(HeaderDef::ChatGroupId) {
return optional_field.clone();
}
if let Some(extracted_grpid) = mime_parser
.get(HeaderDef::MessageId)
.and_then(|value| dc_extract_grpid_from_rfc724_mid(&value))
{
return extracted_grpid.to_string();
}
if !mime_parser.has_chat_version() {
if let Some(extracted_grpid) = extract_grpid(mime_parser, HeaderDef::InReplyTo) {
return extracted_grpid.to_string();
} else if let Some(extracted_grpid) = extract_grpid(mime_parser, HeaderDef::References) {
return extracted_grpid.to_string();
}
}
"".to_string()
}
/// try extract a grpid from a message-id list header value
fn extract_grpid(mime_parser: &MimeMessage, headerdef: HeaderDef) -> Option<&str> {
let header = mime_parser.get(headerdef)?;
@@ -2218,7 +2228,7 @@ mod tests {
let t = TestContext::new_alice().await;
// create one-to-one with bob, archive one-to-one
let bob_id = Contact::create(&t.ctx, "bob", "bob@exampel.org")
let bob_id = Contact::create(&t.ctx, "bob", "bob@example.com")
.await
.unwrap();
let one2one_id = chat::create_by_contact_id(&t.ctx, bob_id).await.unwrap();

View File

@@ -1174,22 +1174,16 @@ mod tests {
assert_eq!(msgs.len(), 1);
// do not repeat the warning every day ...
// (we test that for the 2 subsequent days, this may be the next month, so the result should be 1 or 2 device message)
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + (365 + 1) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
// ... but every month
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + (365 + 31) * 24 * 60 * 60,
timestamp_now + (365 + 2) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
@@ -1197,6 +1191,21 @@ mod tests {
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 2);
let test_len = msgs.len();
assert!(test_len == 1 || test_len == 2);
// ... but every month
// (forward generous 33 days to avoid being in the same month as in the previous check)
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + (365 + 33) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), test_len + 1);
}
}

View File

@@ -457,24 +457,6 @@ impl Imap {
Ok(())
}
async fn get_config_last_seen_uid<S: AsRef<str>>(
&self,
context: &Context,
folder: S,
) -> (u32, u32) {
let key = format!("imap.mailbox.{}", folder.as_ref());
if let Some(entry) = context.sql.get_raw_config(context, &key).await {
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
let mut parts = entry.split(':');
(
parts.next().unwrap_or_default().parse().unwrap_or(0),
parts.next().unwrap_or_default().parse().unwrap_or(0),
)
} else {
(0, 0)
}
}
/// Synchronizes UIDs in the database with UIDs on the server.
///
/// It is assumed that no operations are taking place on the same
@@ -559,7 +541,7 @@ impl Imap {
self.select_folder(context, Some(folder)).await?;
// compare last seen UIDVALIDITY against the current one
let (uid_validity, last_seen_uid) = self.get_config_last_seen_uid(context, &folder).await;
let (uid_validity, last_seen_uid) = get_config_last_seen_uid(context, &folder).await;
let config = &mut self.config;
let mailbox = config
@@ -585,8 +567,7 @@ impl Imap {
// id we do not do this here, we'll miss the first message
// as we will get in here again and fetch from lastseenuid+1 then
self.set_config_last_seen_uid(context, &folder, new_uid_validity, 0)
.await;
set_config_last_seen_uid(context, &folder, new_uid_validity, 0).await;
return Ok((new_uid_validity, 0));
}
@@ -630,8 +611,7 @@ impl Imap {
}
};
self.set_config_last_seen_uid(context, &folder, new_uid_validity, new_last_seen_uid)
.await;
set_config_last_seen_uid(context, &folder, new_uid_validity, new_last_seen_uid).await;
if uid_validity != 0 || last_seen_uid != 0 {
job::schedule_resync(context).await;
}
@@ -714,8 +694,7 @@ impl Imap {
let last_one = new_last_seen_uid.max(new_last_seen_uid_processed);
if last_one > last_seen_uid {
self.set_config_last_seen_uid(context, &folder, uid_validity, last_one)
.await;
set_config_last_seen_uid(context, &folder, uid_validity, last_one).await;
}
if read_errors == 0 {
@@ -861,23 +840,6 @@ impl Imap {
Ok(msgs)
}
async fn set_config_last_seen_uid<S: AsRef<str>>(
&self,
context: &Context,
folder: S,
uidvalidity: u32,
lastseenuid: u32,
) {
let key = format!("imap.mailbox.{}", folder.as_ref());
let val = format!("{}:{}", uidvalidity, lastseenuid);
context
.sql
.set_raw_config(context, &key, Some(&val))
.await
.ok();
}
/// Fetches a list of messages by server UID.
/// The passed in list of uids must be sorted.
///
@@ -1711,6 +1673,36 @@ fn get_fallback_folder(delimiter: &str) -> String {
format!("INBOX{}DeltaChat", delimiter)
}
pub async fn set_config_last_seen_uid<S: AsRef<str>>(
context: &Context,
folder: S,
uidvalidity: u32,
lastseenuid: u32,
) {
let key = format!("imap.mailbox.{}", folder.as_ref());
let val = format!("{}:{}", uidvalidity, lastseenuid);
context
.sql
.set_raw_config(context, &key, Some(&val))
.await
.ok();
}
async fn get_config_last_seen_uid<S: AsRef<str>>(context: &Context, folder: S) -> (u32, u32) {
let key = format!("imap.mailbox.{}", folder.as_ref());
if let Some(entry) = context.sql.get_raw_config(context, &key).await {
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
let mut parts = entry.split(':');
(
parts.next().unwrap_or_default().parse().unwrap_or(0),
parts.next().unwrap_or_default().parse().unwrap_or(0),
)
} else {
(0, 0)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -31,6 +31,7 @@ use crate::param::*;
use crate::pgp;
use crate::sql::{self, Sql};
use crate::stock::StockMessage;
use ::pgp::types::KeyTrait;
use async_tar::Archive;
// Name of the database file in the backup.
@@ -78,11 +79,7 @@ pub enum ImexMode {
///
/// Only one import-/export-progress can run at the same time.
/// To cancel an import-/export-progress, drop the future returned by this function.
pub async fn imex(
context: &Context,
what: ImexMode,
param1: Option<impl AsRef<Path>>,
) -> Result<()> {
pub async fn imex(context: &Context, what: ImexMode, param1: impl AsRef<Path>) -> Result<()> {
let cancel = context.alloc_ongoing().await?;
let res = async {
@@ -413,6 +410,8 @@ async fn set_self_key(
},
)
.await?;
info!(context, "stored self key: {:?}", keypair.secret.key_id());
Ok(())
}
@@ -439,19 +438,11 @@ pub fn normalize_setup_code(s: &str) -> String {
out
}
async fn imex_inner(
context: &Context,
what: ImexMode,
param: Option<impl AsRef<Path>>,
) -> Result<()> {
ensure!(param.is_some(), "No Import/export dir/file given.");
info!(context, "Import/export process started.");
async fn imex_inner(context: &Context, what: ImexMode, path: impl AsRef<Path>) -> Result<()> {
info!(context, "Import/export dir: {}", path.as_ref().display());
ensure!(context.sql.is_open().await, "Database not opened.");
context.emit_event(EventType::ImexProgress(10));
ensure!(context.sql.is_open().await, "Database not opened.");
let path = param.ok_or_else(|| format_err!("Imex: Param was None"))?;
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
// before we export anything, make sure the private key exists
if e2ee::ensure_secret_key_exists(context).await.is_err() {
@@ -875,6 +866,12 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
continue;
}
}
info!(
context,
"considering key file: {}",
path_plus_name.display()
);
match dc_read_file(context, &path_plus_name).await {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
@@ -964,7 +961,7 @@ where
let any_key = key as &dyn Any;
let kind = if any_key.downcast_ref::<SignedPublicKey>().is_some() {
"public"
} else if any_key.downcast_ref::<SignedPublicKey>().is_some() {
} else if any_key.downcast_ref::<SignedSecretKey>().is_some() {
"private"
} else {
"unknown"
@@ -972,7 +969,12 @@ where
let id = id.map_or("default".into(), |i| i.to_string());
dir.as_ref().join(format!("{}-key-{}.asc", kind, &id))
};
info!(context, "Exporting key {}", file_name.display());
info!(
context,
"Exporting key {:?} to {}",
key.key_id(),
file_name.display()
);
dc_delete_file(context, &file_name).await;
let content = key.to_asc(None).into_bytes();
@@ -1040,7 +1042,7 @@ mod tests {
}
#[async_std::test]
async fn test_export_key_to_asc_file() {
async fn test_export_public_key_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().public;
let blobdir = "$BLOBDIR";
@@ -1054,19 +1056,35 @@ mod tests {
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[async_std::test]
async fn test_export_private_key_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().secret;
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
.await
.is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{}/private-key-default.asc", blobdir);
let bytes = async_std::fs::read(&filename).await.unwrap();
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[async_std::test]
async fn test_export_and_import_key() {
let context = TestContext::new().await;
context.configure_alice().await;
let blobdir = "$BLOBDIR";
assert!(imex(&context.ctx, ImexMode::ExportSelfKeys, Some(blobdir))
.await
.is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
assert!(imex(&context.ctx, ImexMode::ImportSelfKeys, Some(blobdir))
.await
.is_ok());
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir).await {
panic!("got error on export: {:?}", err);
}
let context2 = TestContext::new().await;
context2.configure_alice().await;
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir).await {
panic!("got error on import: {:?}", err);
}
}
#[test]

View File

@@ -627,6 +627,9 @@ impl Job {
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
/// and show them in the chat list.
async fn fetch_existing_msgs(&mut self, context: &Context, imap: &mut Imap) -> Status {
if context.get_config_bool(Config::Bot).await {
return Status::Finished(Ok(())); // Bots don't want those messages
}
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
@@ -636,7 +639,7 @@ impl Job {
add_all_recipients_as_contacts(context, imap, Config::ConfiguredMvboxFolder).await;
add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).await;
if context.get_config_bool(Config::FetchExisting).await {
if context.get_config_bool(Config::FetchExistingMsgs).await {
for config in &[
Config::ConfiguredMvboxFolder,
Config::ConfiguredInboxFolder,

View File

@@ -1638,14 +1638,16 @@ pub(crate) async fn handle_ndn(
context: &Context,
failed: &FailureReport,
error: Option<impl AsRef<str>>,
) {
) -> anyhow::Result<()> {
if failed.rfc724_mid.is_empty() {
return;
return Ok(());
}
let res = context
// The NDN might be for a message-id that had attachments and was sent from a non-Delta Chat client.
// In this case we need to mark multiple "msgids" as failed that all refer to the same message-id.
let msgs: Vec<_> = context
.sql
.query_row(
.query_map(
concat!(
"SELECT",
" m.id AS msg_id,",
@@ -1662,37 +1664,42 @@ pub(crate) async fn handle_ndn(
row.get::<_, Chattype>("type")?,
))
},
|rows| Ok(rows.collect::<Vec<_>>()),
)
.await;
if let Err(ref err) = res {
info!(context, "Failed to select NDN {:?}", err);
}
.await?;
if let Ok((msg_id, chat_id, chat_type)) = res {
set_msg_failed(context, msg_id, error).await;
if chat_type == Chattype::Group {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown).await;
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
// Tell the user which of the recipients failed if we know that (because in a group, this might otherwise be unclear)
chat::add_info_msg(
context,
chat_id,
context
.stock_string_repl_str(
StockMessage::FailedSendingTo,
contact.get_display_name(),
)
.await,
)
.await;
context.emit_event(EventType::ChatModified(chat_id));
}
}
for (i, msg) in msgs.into_iter().enumerate() {
let (msg_id, chat_id, chat_type) = msg?;
set_msg_failed(context, msg_id, error.as_ref()).await;
if i == 0 {
// Add only one info msg for all failed messages
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
}
}
Ok(())
}
async fn ndn_maybe_add_info_msg(
context: &Context,
failed: &FailureReport,
chat_id: ChatId,
chat_type: Chattype,
) -> anyhow::Result<()> {
if chat_type == Chattype::Group {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown).await;
let contact = Contact::load_from_db(context, contact_id).await?;
// Tell the user which of the recipients failed if we know that (because in a group, this might otherwise be unclear)
let text = context
.stock_string_repl_str(StockMessage::FailedSendingTo, contact.get_display_name())
.await;
chat::add_info_msg(context, chat_id, text).await;
context.emit_event(EventType::ChatModified(chat_id));
}
}
Ok(())
}
/// The number of messages assigned to real chat (!=deaddrop, !=trash)

View File

@@ -1141,13 +1141,23 @@ async fn build_body_file(
Viewtype::Image | Viewtype::Gif => format!(
"{}.{}",
if base_name.is_empty() {
"image"
chrono::Utc
.timestamp(msg.timestamp_sort as i64, 0)
.format("image_%Y-%m-%d_%H-%M-%S")
.to_string()
} else {
base_name
base_name.to_string()
},
&suffix,
),
Viewtype::Video => format!("video.{}", &suffix),
Viewtype::Video => format!(
"video_{}.{}",
chrono::Utc
.timestamp(msg.timestamp_sort as i64, 0)
.format("%Y-%m-%d_%H-%M-%S")
.to_string(),
&suffix
),
_ => blob.as_file_name().to_string(),
};

View File

@@ -295,7 +295,8 @@ impl MimeMessage {
/// Squashes mutlipart chat messages with attachment into single-part messages.
///
/// 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 a description. If such a message is detected, text from the first part can be
/// moved to the second part, and the first part dropped.
#[allow(clippy::indexing_slicing)]
fn squash_attachment_parts(&mut self) {
if let [textpart, filepart] = &self.parts[..] {
@@ -315,6 +316,9 @@ impl MimeMessage {
// insert new one
filepart.msg = self.parts[0].msg.clone();
if let Some(quote) = self.parts[0].param.get(Param::Quote) {
filepart.param.set(Param::Quote, quote);
}
// forget the one we use now
self.parts[0].msg = "".to_string();
@@ -1071,7 +1075,9 @@ impl MimeMessage {
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
message::handle_ndn(context, failure_report, error).await
if let Err(e) = message::handle_ndn(context, failure_report, error).await {
warn!(context, "Could not handle ndn: {}", e);
}
}
}
@@ -2266,4 +2272,53 @@ From: alice <alice@example.org>
);
assert_eq!(message.parts[0].msg, "");
}
#[async_std::test]
async fn parse_quote_top_posting() {
let context = TestContext::new().await;
let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Subject: Re: top posting
MIME-Version: 1.0
In-Reply-To: <bar@example.org>
Message-ID: <foo@example.org>
To: bob <bob@example.org>
From: alice <alice@example.org>
A reply.
On 2020-10-25, Bob wrote:
> A quote.
"##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(message.get_subject(), Some("Re: top posting".to_string()));
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::Text);
assert_eq!(
message.parts[0].param.get(Param::Quote).unwrap(),
"A quote."
);
assert_eq!(message.parts[0].msg, "A reply.");
}
#[async_std::test]
async fn test_attachment_quote() {
let context = TestContext::new().await;
let raw = include_bytes!("../test-data/message/quote_attach.eml");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(mimeparser.get_subject().unwrap(), "Message from Alice");
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].msg, "Reply");
assert_eq!(
mimeparser.parts[0].param.get(Param::Quote).unwrap(),
"Quote"
);
assert_eq!(mimeparser.parts[0].typ, Viewtype::File);
}
}

View File

@@ -32,6 +32,7 @@ static P_AKTIVIX_ORG: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -46,6 +47,7 @@ static P_AOL: Lazy<Provider> = Lazy::new(|| {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
@@ -74,6 +76,7 @@ static P_ARCOR_DE: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -101,6 +104,7 @@ static P_AUTISTICI_ORG: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -128,6 +132,7 @@ static P_BLUEWIN_CH: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -141,20 +146,21 @@ static P_BUZON_UY: Lazy<Provider> = Lazy::new(|| Provider {
Server {
protocol: IMAP,
socket: STARTTLS,
hostname: "buzon.uy",
hostname: "mail.buzon.uy",
port: 143,
username_pattern: EMAIL,
},
Server {
protocol: SMTP,
socket: STARTTLS,
hostname: "buzon.uy",
hostname: "mail.buzon.uy",
port: 587,
username_pattern: EMAIL,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -182,6 +188,7 @@ static P_CHELLO_AT: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -194,6 +201,7 @@ static P_COMCAST: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -206,6 +214,7 @@ static P_DISMAIL_DE: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -218,6 +227,7 @@ static P_DISROOT: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -269,6 +279,7 @@ static P_DUBBY_ORG: Lazy<Provider> = Lazy::new(|| Provider {
},
]),
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -285,6 +296,7 @@ static P_EXAMPLE_COM: Lazy<Provider> = Lazy::new(|| {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
@@ -299,6 +311,7 @@ static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -313,6 +326,7 @@ static P_FIREMAIL_DE: Lazy<Provider> = Lazy::new(|| {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
@@ -343,6 +357,7 @@ static P_FIVE_CHAT: Lazy<Provider> = Lazy::new(|| Provider {
},
]),
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -370,6 +385,7 @@ static P_FREENET_DE: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -386,6 +402,7 @@ static P_GMAIL: Lazy<Provider> = Lazy::new(|| {
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: Some(Oauth2Authorizer::Gmail),
}
});
@@ -421,6 +438,7 @@ static P_GMX_NET: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -450,6 +468,7 @@ static P_HERMES_RADIO: Lazy<Provider> = Lazy::new(|| Provider {
},
]),
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -464,6 +483,7 @@ static P_HEY_COM: Lazy<Provider> = Lazy::new(|| {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
@@ -477,6 +497,7 @@ static P_I_UA: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -505,6 +526,7 @@ static P_ICLOUD: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -517,6 +539,7 @@ static P_KOLST_COM: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -529,6 +552,7 @@ static P_KONTENT_COM: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -541,6 +565,7 @@ static P_MAIL_RU: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -553,6 +578,7 @@ static P_MAILBOX_ORG: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -575,9 +601,10 @@ static P_NAUTA_CU: Lazy<Provider> = Lazy::new(|| {
ConfigDefault { key: Config::MvboxMove, value: "0" },
ConfigDefault { key: Config::E2eeEnabled, value: "0" },
ConfigDefault { key: Config::MediaQuality, value: "1" },
ConfigDefault { key: Config::FetchExisting, value: "0" },
ConfigDefault { key: Config::FetchExistingMsgs, value: "0" },
]),
strict_tls: false,
max_smtp_rcpt_to: Some(20),
oauth2_authorizer: None,
}
});
@@ -595,6 +622,7 @@ static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
@@ -623,6 +651,7 @@ static P_POSTEO: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -637,6 +666,7 @@ static P_PROTONMAIL: Lazy<Provider> = Lazy::new(|| {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
@@ -650,6 +680,7 @@ static P_RISEUP_NET: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -662,6 +693,7 @@ static P_ROGERS_COM: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -674,6 +706,7 @@ static P_SYSTEMLI_ORG: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -688,6 +721,7 @@ static P_T_ONLINE: Lazy<Provider> = Lazy::new(|| {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
@@ -740,6 +774,7 @@ static P_TESTRUN: Lazy<Provider> = Lazy::new(|| Provider {
},
]),
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -767,6 +802,7 @@ static P_TISCALI_IT: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -779,6 +815,7 @@ static P_UKR_NET: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -806,6 +843,7 @@ static P_UNDERNET_UY: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -818,6 +856,7 @@ static P_VFEMAIL: Lazy<Provider> = Lazy::new(|| Provider {
server: vec![],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -845,6 +884,7 @@ static P_VODAFONE_DE: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -862,6 +902,7 @@ static P_WEB_DE: Lazy<Provider> = Lazy::new(|| {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
@@ -879,6 +920,7 @@ static P_YAHOO: Lazy<Provider> = Lazy::new(|| {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
@@ -907,6 +949,7 @@ static P_YANDEX_RU: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: Some(Oauth2Authorizer::Yandex),
});
@@ -934,6 +977,7 @@ static P_ZIGGO_NL: Lazy<Provider> = Lazy::new(|| Provider {
],
config_defaults: None,
strict_tls: false,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
@@ -1109,4 +1153,4 @@ pub static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy:
});
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd(2020, 10, 17));
Lazy::new(|| chrono::NaiveDate::from_ymd(2020, 10, 30));

View File

@@ -75,6 +75,7 @@ pub struct Provider {
pub server: Vec<Server>,
pub config_defaults: Option<Vec<ConfigDefault>>,
pub strict_tls: bool,
pub max_smtp_rcpt_to: Option<u16>,
pub oauth2_authorizer: Option<Oauth2Authorizer>,
}

View File

@@ -104,6 +104,9 @@ def process_data(data, file):
strict_tls = data.get("strict_tls", False)
strict_tls = "true" if strict_tls else "false"
max_smtp_rcpt_to = data.get("max_smtp_rcpt_to", 0)
max_smtp_rcpt_to = "Some(" + str(max_smtp_rcpt_to) + ")" if max_smtp_rcpt_to != 0 else "None"
oauth2 = data.get("oauth2", "")
oauth2 = "Some(Oauth2Authorizer::" + camel(oauth2) + ")" if oauth2 != "" else "None"
@@ -119,6 +122,7 @@ def process_data(data, file):
provider += " server: vec![\n" + server + " ],\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " strict_tls: " + strict_tls + ",\n"
provider += " max_smtp_rcpt_to: " + max_smtp_rcpt_to + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
provider += "});\n\n"
else:

View File

@@ -69,9 +69,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
}
Some(job) => {
// Let the fetch run, but return back to the job afterwards.
info!(ctx, "postponing imap-job {} to run fetch...", job);
jobs_loaded = 0;
fetch(&ctx, &mut connection).await;
if ctx.get_config_bool(Config::InboxWatch).await {
info!(ctx, "postponing imap-job {} to run fetch...", job);
fetch(&ctx, &mut connection).await;
}
}
None => {
jobs_loaded = 0;

View File

@@ -71,7 +71,7 @@ pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool, Opti
let lines = split_lines(&input);
let (lines, is_forwarded) = skip_forward_header(&lines);
let (lines, top_quote) = remove_top_quote(lines);
let (lines, mut top_quote) = remove_top_quote(lines);
let original_lines = &lines;
let lines = remove_message_footer(lines);
@@ -79,12 +79,16 @@ pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool, Opti
render_message(lines, false)
} else {
let (lines, has_nonstandard_footer) = remove_nonstandard_footer(lines);
let (lines, has_bottom_quote) = remove_bottom_quote(lines);
let (lines, mut bottom_quote) = remove_bottom_quote(lines);
if top_quote.is_none() && bottom_quote.is_some() {
std::mem::swap(&mut top_quote, &mut bottom_quote);
}
if lines.iter().all(|it| it.trim().is_empty()) {
render_message(original_lines, false)
} else {
render_message(lines, has_nonstandard_footer || has_bottom_quote)
render_message(lines, has_nonstandard_footer || bottom_quote.is_some())
}
};
(text, is_forwarded, top_quote)
@@ -105,16 +109,27 @@ fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
}
#[allow(clippy::indexing_slicing)]
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
let mut first_quoted_line = lines.len();
let mut last_quoted_line = None;
for (l, line) in lines.iter().enumerate().rev() {
if is_plain_quote(line) {
if last_quoted_line.is_none() {
first_quoted_line = l + 1;
}
last_quoted_line = Some(l)
} else if !is_empty_line(line) {
break;
}
}
if let Some(mut l_last) = last_quoted_line {
let quoted_text = lines[l_last..first_quoted_line]
.iter()
.map(|s| {
s.strip_prefix(">")
.map_or(*s, |u| u.strip_prefix(" ").unwrap_or(u))
})
.join("\n");
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
l_last -= 1
}
@@ -124,9 +139,9 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
l_last -= 1
}
}
(&lines[..l_last], true)
(&lines[..l_last], Some(quoted_text))
} else {
(lines, false)
(lines, None)
}
}

View File

@@ -192,7 +192,8 @@ impl Smtp {
};
let security = match lp.security {
Socket::STARTTLS | Socket::Plain => smtp::ClientSecurity::Opportunistic(tls_parameters),
Socket::Plain => smtp::ClientSecurity::None,
Socket::STARTTLS => smtp::ClientSecurity::Required(tls_parameters),
_ => smtp::ClientSecurity::Wrapper(tls_parameters),
};

View File

@@ -3,8 +3,11 @@
use super::Smtp;
use async_smtp::*;
use crate::config::Config;
use crate::constants::DEFAULT_MAX_SMTP_RCPT_TO;
use crate::context::Context;
use crate::events::EventType;
use crate::provider::get_provider_info;
use itertools::Itertools;
use std::time::Duration;
@@ -34,37 +37,51 @@ impl Smtp {
) -> Result<()> {
let message_len_bytes = message.len();
let recipients_display = recipients.iter().map(|x| x.to_string()).join(",");
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
message,
);
if let Some(ref mut transport) = self.transport {
// The timeout is 1min + 3min per MB.
let timeout = 60 + (180 * message_len_bytes / 1_000_000) as u64;
transport
.send_with_timeout(mail, Some(&Duration::from_secs(timeout)))
let mut chunk_size = DEFAULT_MAX_SMTP_RCPT_TO;
if let Some(provider) = get_provider_info(
&context
.get_config(Config::ConfiguredAddr)
.await
.map_err(Error::SendError)?;
context.emit_event(EventType::SmtpMessageSent(format!(
"Message len={} was smtp-sent to {}",
message_len_bytes, recipients_display
)));
self.last_success = Some(std::time::SystemTime::now());
Ok(())
} else {
warn!(
context,
"uh? SMTP has no transport, failed to send to {}", recipients_display
);
Err(Error::NoTransport)
.unwrap_or_default(),
) {
if let Some(max_smtp_rcpt_to) = provider.max_smtp_rcpt_to {
chunk_size = max_smtp_rcpt_to as usize;
}
}
for recipients_chunk in recipients.chunks(chunk_size).into_iter() {
let recipients = recipients_chunk.to_vec();
let recipients_display = recipients.iter().map(|x| x.to_string()).join(",");
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
&message,
);
if let Some(ref mut transport) = self.transport {
// The timeout is 1min + 3min per MB.
let timeout = 60 + (180 * message_len_bytes / 1_000_000) as u64;
transport
.send_with_timeout(mail, Some(&Duration::from_secs(timeout)))
.await
.map_err(Error::SendError)?;
context.emit_event(EventType::SmtpMessageSent(format!(
"Message len={} was smtp-sent to {}",
message_len_bytes, recipients_display
)));
self.last_success = Some(std::time::SystemTime::now());
} else {
warn!(
context,
"uh? SMTP has no transport, failed to send to {}", recipients_display
);
return Err(Error::NoTransport);
}
}
Ok(())
}
}

View File

@@ -244,6 +244,10 @@ pub enum StockMessage {
// used in summaries, a noun, not a verb (not: "to reply")
#[strum(props(fallback = "Reply"))]
ReplyNoun = 90,
#[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\
To use the \"Saved messages\" feature again, create a new chat with yourself."))]
SelfDeletedMsgBody = 91,
}
/*
@@ -463,6 +467,7 @@ mod tests {
use crate::constants::DC_CONTACT_ID_SELF;
use crate::chat::Chat;
use crate::chatlist::Chatlist;
use num_traits::ToPrimitive;
@@ -654,8 +659,31 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2);
chats.get_chat_id(0).delete(&t.ctx).await.ok();
chats.get_chat_id(1).delete(&t.ctx).await.ok();
let chat0 = Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
.await
.unwrap();
let (self_talk_id, device_chat_id) = if chat0.is_self_talk() {
(chats.get_chat_id(0), chats.get_chat_id(1))
} else {
(chats.get_chat_id(1), chats.get_chat_id(0))
};
// delete self-talk first; this adds a message to device-chat about how self-talk can be restored
let device_chat_msgs_before = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None)
.await
.len();
self_talk_id.delete(&t.ctx).await.ok();
assert_eq!(
chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None)
.await
.len(),
device_chat_msgs_before + 1
);
// delete device chat
device_chat_id.delete(&t.ctx).await.ok();
// check, that the chatlist is empty
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);

View File

@@ -0,0 +1,27 @@
Subject: Message from Alice
MIME-Version: 1.0
In-Reply-To: <Mr.7h_HyOuM3Dz.xcp4v8QiQua@testrun.org>
Date: Sun, 08 Nov 2020 01:16:26 +0000
Chat-Version: 1.0
Message-ID: <Mr.126R7OsBKvk.dGGBC5WcsLF@testrun.org>
To: Bob <bob@example.org>
From: Alice <alice@testrun.org>
Content-Type: multipart/mixed; boundary="uWbWY2IyEtJ8wZmp282Na11hxBBXlV"
--uWbWY2IyEtJ8wZmp282Na11hxBBXlV
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
> Quote
Reply
--uWbWY2IyEtJ8wZmp282Na11hxBBXlV
Content-Type: text/plain
Content-Disposition: attachment; filename="attachment.txt"
Content-Transfer-Encoding: base64
ZGF0YSB0byBzZW5k
--uWbWY2IyEtJ8wZmp282Na11hxBBXlV--