Compare commits

..

15 Commits

Author SHA1 Message Date
dignifiedquire
97afe582e1 fix: dont use self.error when it is not available 2020-01-29 18:54:43 +01:00
=
4498954918 simplify failure 2020-01-29 17:31:20 +00:00
=
61ff2c4bee add a failing test taht doesn't use pytest anymore 2020-01-29 17:14:17 +00:00
=
14902ecf15 another iteration 2020-01-29 17:01:47 +00:00
=
289d8fdabf simplify thread handling 2020-01-29 16:48:09 +00:00
=
26c39f876b correctly use dc_context_new 2020-01-29 16:43:01 +00:00
=
1f53d489cb snapshot to try to get test_lowlevel.py to pass 2020-01-29 16:36:00 +00:00
holger krekel
35d055ea62 fix lint 2020-01-29 16:25:39 +01:00
dignifiedquire
a2c94fc715 add threads start & stop 2020-01-29 16:05:39 +01:00
dignifiedquire
e93911effb update ffi 2020-01-29 15:44:20 +01:00
dignifiedquire
41a7e06332 refactor: make run take the callback and avoid boxing 2020-01-29 14:19:36 +01:00
dignifiedquire
29bb2ec58b some fixes 2020-01-29 14:09:21 +01:00
dignifiedquire
efcad4126d cargo fmt 2020-01-29 14:09:21 +01:00
dignifiedquire
b4a5767347 start integration work 2020-01-29 14:09:21 +01:00
dignifiedquire
4dbcab9e6d feat: implement minimal rust threads 2020-01-29 14:08:35 +01:00
91 changed files with 2663 additions and 5640 deletions

View File

@@ -15,7 +15,6 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: check
args: --workspace --examples --tests --all-features
fmt:
name: Rustfmt

View File

@@ -1,52 +1,5 @@
# Changelog
## 1.25.0
- save traffic by downloading only messages that are really displayed #1236
- change generated key type to Ed25519, these keys are much shorter
than RSA keys, which results in saving traffic and speed improvements #1287
- improve key handling #1237 #1240 #1242 #1247
- mute handling, apis are dc_set_chat_mute_duration()
dc_chat_is_muted() and dc_chat_get_remaining_mute_duration() #1143
- pinning chats, new apis are dc_set_chat_visibility() and
dc_chat_get_visibility() #1248
- add dc_provider_new_from_email() api that queries the new, integrated
provider-database #1207
- account creation by scanning a qr code
in the DCACCOUNT scheme (https://mailadm.readthedocs.io),
new api is dc_set_config_from_qr() #1249
- if possible, dc_join_securejoin(), returns the new chat-id immediately
and does the handshake in background #1225
- update imap and smtp dependencies #1115
- check for MOVE capability before using MOVE command #1263
- allow inline attachments from RFC 2183 #1280
- fix updating names from incoming mails #1298
- fix error messages shown on import #1234
- directly attempt to re-connect if the smtp connection is maybe stale #1296
- improve adding group members #1291
- improve rust-api #1261
- cleanup #1302 #1283 #1282 #1276 #1270-#1274 #1267 #1258-#1260
#1257 #1239 #1231 #1224
- update spec #1286 #1291
## 1.0.0-beta.24
- fix oauth2/gmail bug introduced in beta23 (not used in releases) #1219

989
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
[package]
name = "deltachat"
version = "1.25.0"
version = "1.0.0-beta.24"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
[profile.release]
lto = true
# lto = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
@@ -58,8 +58,8 @@ encoded-words = { git = "https://github.com/async-email/encoded-words", branch="
native-tls = "0.2.3"
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
pretty_env_logger = "0.3.1"
rustyline = { version = "4.1.0", optional = true }
crossbeam = "0.7.3"
[dev-dependencies]
tempfile = "3.0"

View File

@@ -6,7 +6,7 @@
set -e -x
# for core-building and python install step
export DCC_RS_TARGET=debug
export DCC_RS_TARGET=release
export DCC_RS_DEV=`pwd`
cd python

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.25.0"
version = "1.0.0-beta.24"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -16,6 +16,7 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-provider-database = "0.2.1"
libc = "0.2"
human-panic = "1.0.1"
num-traits = "0.2.6"

View File

@@ -243,7 +243,7 @@ typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t
* The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage.
*/
dc_context_t* dc_context_new (dc_callback_t cb, void* userdata, const char* os_name);
dc_context_t* dc_context_new (void* userdata, const char* os_name);
/**
@@ -364,12 +364,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* also show all mails of confirmed contacts,
* DC_SHOW_EMAILS_ALL (2)=
* also show mails of unconfirmed contacts in the deaddrop.
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
* generate recommended key type (default),
* DC_KEY_GEN_RSA2048 (1)=
* generate RSA 2048 keypair
* DC_KEY_GEN_ED25519 (2)=
* generate Ed25519 keypair
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
@@ -409,7 +403,6 @@ int dc_set_config (dc_context_t* context, const char*
*/
char* dc_get_config (dc_context_t* context, const char* key);
/**
* Set stock string translation.
*
@@ -424,22 +417,6 @@ char* dc_get_config (dc_context_t* context, const char*
int dc_set_stock_translation(dc_context_t* context, uint32_t stock_id, const char* stock_msg);
/**
* Set configuration values from a QR code containing an account.
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT.
*
* Internally, the function will call dc_set_config()
* at least with the keys `addr` and `mail_pw`.
*
* @memberof dc_context_t
* @param context The context object
* @param qr scanned QR code
* @return int (==0 on error, 1 on success)
*/
int dc_set_config_from_qr (dc_context_t* context, const char* qr);
/**
* Get information about the context.
*
@@ -561,307 +538,14 @@ int dc_is_configured (const dc_context_t* context);
/**
* Execute pending imap-jobs.
* This function and dc_perform_imap_fetch() and dc_perform_imap_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* imap_thread_func(void* context)
* {
* while (true) {
* dc_perform_imap_jobs(context);
* dc_perform_imap_fetch(context);
* dc_perform_imap_idle(context);
* }
* }
*
* // start imap-thread that runs forever
* pthread_t imap_thread;
* pthread_create(&imap_thread, NULL, imap_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_imap_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
* TODO: Document
*/
void dc_perform_imap_jobs (dc_context_t* context);
void dc_context_run (dc_context_t* context, dc_callback_t cb);
/**
* Fetch new messages, if any.
* This function and dc_perform_imap_jobs() and dc_perform_imap_idle() must be called from the same thread,
* typically in a loop.
*
* See dc_perform_imap_jobs() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
* TODO: Document
*/
void dc_perform_imap_fetch (dc_context_t* context);
/**
* Wait for messages or jobs.
* This function and dc_perform_imap_jobs() and dc_perform_imap_fetch() must be called from the same thread,
* typically in a loop.
*
* You should call this function directly after calling dc_perform_imap_fetch().
*
* See dc_perform_imap_jobs() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_imap_idle (dc_context_t* context);
/**
* Interrupt waiting for imap-jobs.
* If dc_perform_imap_jobs(), dc_perform_imap_fetch() and dc_perform_imap_idle() are called in a loop,
* calling this function causes imap-jobs to be executed and messages to be fetched.
*
* dc_interrupt_imap_idle() does _not_ interrupt dc_perform_imap_jobs() or dc_perform_imap_fetch().
* If the imap-thread is inside one of these functions when dc_interrupt_imap_idle() is called, however,
* the next call of the imap-thread to dc_perform_imap_idle() is interrupted immediately.
*
* Internally, this function is called whenever a imap-jobs should be processed
* (delete message, markseen etc.).
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_imap_idle (dc_context_t* context);
/**
* Execute pending mvbox-jobs.
* This function and dc_perform_mvbox_fetch() and dc_perform_mvbox_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* mvbox_thread_func(void* context)
* {
* while (true) {
* dc_perform_mvbox_jobs(context);
* dc_perform_mvbox_fetch(context);
* dc_perform_mvbox_idle(context);
* }
* }
*
* // start mvbox-thread that runs forever
* pthread_t mvbox_thread;
* pthread_create(&mvbox_thread, NULL, mvbox_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_mvbox_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_jobs (dc_context_t* context);
/**
* Fetch new messages from the MVBOX, if any.
* The MVBOX is a folder on the account where chat messages are moved to.
* The moving is done to not disturb shared accounts that are used by both,
* Delta Chat and a classical MUA.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_fetch (dc_context_t* context);
/**
* Wait for messages or jobs in the MVBOX-thread.
* This function and dc_perform_mvbox_fetch().
* must be called from the same thread, typically in a loop.
*
* You should call this function directly after calling dc_perform_mvbox_fetch().
*
* See dc_perform_mvbox_fetch() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_idle (dc_context_t* context);
/**
* Interrupt waiting for MVBOX-fetch.
* dc_interrupt_mvbox_idle() does _not_ interrupt dc_perform_mvbox_fetch().
* If the MVBOX-thread is inside this function when dc_interrupt_mvbox_idle() is called, however,
* the next call of the MVBOX-thread to dc_perform_mvbox_idle() is interrupted immediately.
*
* Internally, this function is called whenever a imap-jobs should be processed.
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_mvbox_idle (dc_context_t* context);
/**
* Execute pending sentbox-jobs.
* This function and dc_perform_sentbox_fetch() and dc_perform_sentbox_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* sentbox_thread_func(void* context)
* {
* while (true) {
* dc_perform_sentbox_jobs(context);
* dc_perform_sentbox_fetch(context);
* dc_perform_sentbox_idle(context);
* }
* }
*
* // start sentbox-thread that runs forever
* pthread_t sentbox_thread;
* pthread_create(&sentbox_thread, NULL, sentbox_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_sentbox_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_jobs (dc_context_t* context);
/**
* Fetch new messages from the Sent folder, if any.
* This function and dc_perform_sentbox_idle()
* must be called from the same thread, typically in a loop.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_fetch (dc_context_t* context);
/**
* Wait for messages or jobs in the SENTBOX-thread.
* This function and dc_perform_sentbox_fetch()
* must be called from the same thread, typically in a loop.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_idle (dc_context_t* context);
/**
* Interrupt waiting for messages or jobs in the SENTBOX-thread.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_sentbox_idle (dc_context_t* context);
/**
* Execute pending smtp-jobs.
* This function and dc_perform_smtp_idle() must be called from the same thread,
* typically in a loop.
*
* Example:
*
* void* smtp_thread_func(void* context)
* {
* while (true) {
* dc_perform_smtp_jobs(context);
* dc_perform_smtp_idle(context);
* }
* }
*
* // start smtp-thread that runs forever
* pthread_t smtp_thread;
* pthread_create(&smtp_thread, NULL, smtp_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_smtp_idle() in the thread above
* // to return so that jobs are executed
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_smtp_jobs (dc_context_t* context);
/**
* Wait for smtp-jobs.
* This function and dc_perform_smtp_jobs() must be called from the same thread,
* typically in a loop.
*
* See dc_interrupt_smtp_idle() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_smtp_idle (dc_context_t* context);
/**
* Interrupt waiting for smtp-jobs.
* If dc_perform_smtp_jobs() and dc_perform_smtp_idle() are called in a loop,
* calling this function causes jobs to be executed.
*
* dc_interrupt_smtp_idle() does _not_ interrupt dc_perform_smtp_jobs().
* If the smtp-thread is inside this function when dc_interrupt_smtp_idle() is called, however,
* the next call of the smtp-thread to dc_perform_smtp_idle() is interrupted immediately.
*
* Internally, this function is called whenever a message is to be sent.
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_smtp_idle (dc_context_t* context);
void dc_context_shutdown (dc_context_t* context);
/**
* This function can be called whenever there is a hint
@@ -875,27 +559,6 @@ void dc_interrupt_smtp_idle (dc_context_t* context);
void dc_maybe_network (dc_context_t* context);
/**
* Save a keypair as the default keys for the user.
*
* This API is only for testing purposes and should not be used as part of a
* normal application, use the import-export APIs instead.
*
* This saves a public/private keypair as the default keypair in the context.
* It allows avoiding having to generate a secret key for unittests which need
* one.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param addr The email address of the user. This must match the
* configured_addr setting of the context as well as the UID of the key.
* @param public_data The public key as base64.
* @param secret_data The secret key as base64.
* @return 1 on success, 0 on failure.
*/
int dc_preconfigure_keypair (dc_context_t* context, const char *addr, const char *public_data, const char *secret_data);
// handle chatlists
#define DC_GCL_ARCHIVED_ONLY 0x01
@@ -924,7 +587,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
* or "Not now".
* The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
* - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
* archived _any_ chat using dc_set_chat_visibility(). The UI should show a link as
* archived _any_ chat using dc_archive_chat(). The UI should show a link as
* "Show archived chats", if the user clicks this item, the UI should show a
* list of all archived chats that can be created by this function hen using
* the DC_GCL_ARCHIVED_ONLY flag.
@@ -1378,18 +1041,25 @@ uint32_t dc_get_next_media (dc_context_t* context, uint32_t ms
/**
* Set chat visibility to pinned, archived or normal.
* Archive or unarchive a chat.
*
* Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED
* See @ref DC_CHAT_VISIBILITY for detailed information about the visibilities.
* 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.
*
* - To get a list of archived chats, use dc_get_chatlist() with the flag DC_GCL_ARCHIVED_ONLY.
* - To find out the archived state of a given chat, use dc_chat_get_archived()
* - Messages in archived chats are marked as being noticed, so they do not count as "fresh"
* - Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to change the visibility for.
* @param visibility one of @ref DC_CHAT_VISIBILITY
* @param chat_id The ID of the chat to archive or unarchive.
* @param archive 1=archive chat, 0=unarchive chat, all other values are reserved for future use
* @return None.
*/
void dc_set_chat_visibility (dc_context_t* context, uint32_t chat_id, int visibility);
void dc_archive_chat (dc_context_t* context, uint32_t chat_id, int archive);
/**
@@ -1593,22 +1263,6 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
int dc_set_chat_profile_image (dc_context_t* context, uint32_t chat_id, const char* image);
/**
* Set mute duration of a chat.
*
* This value can be checked by the ui upon receiving a new message to decide whether it should trigger an notification.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*
* @memberof dc_context_t
* @param chat_id The chat ID to set the mute duration.
* @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds)
* @param context The context as created by dc_context_new().
* @return 1=success, 0=error
*/
int dc_set_chat_mute_duration (dc_context_t* context, uint32_t chat_id, int64_t duration);
// handle messages
/**
@@ -2127,7 +1781,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_FPR_OK 210 // id=contact
#define DC_QR_FPR_MISMATCH 220 // id=contact
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
#define DC_QR_ACCOUNT 250 // text1=domain
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
@@ -2145,12 +1798,12 @@ void dc_stop_ongoing_process (dc_context_t* context);
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
* - DC_QR_ACCOUNT allows creation of an account, dc_lot_t::text1=domain
* - DC_QR_ADDR with dc_lot_t::id=Contact ID
* - DC_QR_TEXT with dc_lot_t::text1=Text
* - DC_QR_URL with dc_lot_t::text1=URL
* - DC_QR_ERROR with dc_lot_t::text1=Error string
*
*
* @memberof dc_context_t
* @param context The context object.
* @param qr The text of the scanned QR code.
@@ -2845,14 +2498,21 @@ uint32_t dc_chat_get_color (const dc_chat_t* chat);
/**
* Get visibility of chat.
* See @ref DC_CHAT_VISIBILITY for detailed information about the visibilities.
* Get archived state.
*
* - 0 = normal chat, not archived, not sticky.
* - 1 = chat archived
* - 2 = chat sticky (reserved for future use, if you do not support this value, just treat the chat as a normal one)
*
* To archive or unarchive chats, use dc_archive_chat().
* If chats are archived, this should be shown in the UI by a little icon or text,
* eg. the search will also return archived chats.
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return One of @ref DC_CHAT_VISIBILITY
* @return Archived state.
*/
int dc_chat_get_visibility (const dc_chat_t* chat);
int dc_chat_get_archived (const dc_chat_t* chat);
/**
@@ -2945,26 +2605,6 @@ int dc_chat_is_verified (const dc_chat_t* chat);
int dc_chat_is_sending_locations (const dc_chat_t* chat);
/**
* Check whether the chat is currently muted
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=muted, 0=not muted
*/
int dc_chat_is_muted (const dc_chat_t* chat);
/**
* Get the exact state of the mute of a chat
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 0=not muted, -1=forever muted, (x>0)=remaining seconds until the mute is lifted
*/
int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
/**
* @class dc_msg_t
*
@@ -3753,19 +3393,30 @@ int dc_contact_is_verified (dc_contact_t* contact);
*/
/**
* Create a provider struct for the given domain.
*
* @memberof dc_provider_t
* @param domain The domain to get provider info for.
* @return a dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_domain (const char* domain);
/**
* Create a provider struct for the given email address.
*
* The provider is extracted from the email address and it's information is returned.
*
* @memberof dc_provider_t
* @param context The context object as created by dc_context_new().
* @param email The user's email address to extract the provider info form.
* @return a dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_email (const dc_context_t* context, const char* email);
dc_provider_t* dc_provider_new_from_email (const char* email);
/**
@@ -3775,35 +3426,53 @@ dc_provider_t* dc_provider_new_from_email (const dc_context_t* conte
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return String with a fully-qualified URL,
* if there is no such URL, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_overview_page (const dc_provider_t* provider);
/**
* 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 provider's name.
*
* Moreover, the ui should display a "More information" link
* that forwards to the url returned by dc_provider_get_overview_page().
* The name of the provider, e.g. "POSTEO".
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string with the hint to show to the user, may contain multiple lines,
* if there is no such hint, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_before_login_hint (const dc_provider_t* provider);
char* dc_provider_get_name (const dc_provider_t* provider);
/**
* The markdown content of the providers page.
*
* This contains the preparation steps or additional information if the status
* is @ref DC_PROVIDER_STATUS_BROKEN.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_markdown (const dc_provider_t* provider);
/**
* Date of when the state was last checked/updated.
*
* This is returned as a string.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_status_date (const dc_provider_t* provider);
/**
* Whether DC works with this provider.
*
* Can be one of #DC_PROVIDER_STATUS_OK,
* #DC_PROVIDER_STATUS_PREPARATION or #DC_PROVIDER_STATUS_BROKEN.
* Can be one of @ref DC_PROVIDER_STATUS_OK, @ref
* DC_PROVIDER_STATUS_PREPARATION and @ref DC_PROVIDER_STATUS_BROKEN.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
@@ -3818,7 +3487,7 @@ int dc_provider_get_status (const dc_provider_t* prov
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
*/
void dc_provider_unref (dc_provider_t* provider);
void dc_provider_unref (const dc_provider_t* provider);
/**
@@ -4524,8 +4193,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore
char* dc_get_version_str (void); // deprecated
void dc_array_add_id (dc_array_t*, uint32_t); // deprecated
#define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore
#define dc_chat_get_archived(a) (dc_chat_get_visibility((a))==1? 1 : 0) // not used anymore
/*
@@ -4535,13 +4202,6 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
#define DC_SHOW_EMAILS_ACCEPTED_CONTACTS 1
#define DC_SHOW_EMAILS_ALL 2
/*
* Values for dc_get|set_config("key_gen_type")
*/
#define DC_KEY_GEN_DEFAULT 0
#define DC_KEY_GEN_RSA2048 1
#define DC_KEY_GEN_ED25519 2
/**
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
@@ -4553,43 +4213,23 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
*/
/**
* Prover works out-of-the-box.
* This provider status is returned for provider where the login
* works by just entering the name or the email-address.
* Provider status returned by dc_provider_get_status().
*
* - 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.
* - There is no need for the user to enter advanced settings;
* server, port etc. are known by the core.
*
* The status is returned by dc_provider_get_status().
* Works right out of the box without any preperation steps needed
*/
#define DC_PROVIDER_STATUS_OK 1
/**
* Provider works, but there are preparations needed.
* Provider status returned by dc_provider_get_status().
*
* - 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()
* 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.
*
* The status is returned by dc_provider_get_status().
* Works, but preparation steps are needed
*/
#define DC_PROVIDER_STATUS_PREPARATION 2
/**
* 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.
* Provider status returned by dc_provider_get_status().
*
* More information about that is typically provided
* in the string returned by dc_provider_get_before_login_hints()
* and in the page linked by dc_provider_get_overview_page().
*
* The status is returned by dc_provider_get_status().
* Doesn't work (too unstable to use falls also in this category)
*/
#define DC_PROVIDER_STATUS_BROKEN 3
@@ -4598,48 +4238,6 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
*/
/**
* @defgroup DC_CHAT_VISIBILITY DC_CHAT_VISIBILITY
*
* These constants describe the visibility of a chat.
* The chat visibiliry can be get using dc_chat_get_visibility()
* and set using dc_set_chat_visibility().
*
* @addtogroup DC_CHAT_VISIBILITY
* @{
*/
/**
* Chats with normal visibility are not archived and are shown below all pinned chats.
* Archived chats, that receive new messages automatically become normal chats.
*/
#define DC_CHAT_VISIBILITY_NORMAL 0
/**
* 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.
*
* 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.
*
* If archived chats receive new messages, they become normal chats again.
*
* To get a list of archived chats, use dc_get_chatlist() with the flag DC_GCL_ARCHIVED_ONLY.
*/
#define DC_CHAT_VISIBILITY_ARCHIVED 1
/**
* Pinned chats are included in the default chatlist. moreover,
* they are always the first items, whether they have fresh messages or not.
*/
#define DC_CHAT_VISIBILITY_PINNED 2
/**
* @}
*/
/*
* TODO: Strings need some doumentation about used placeholders.
*

View File

@@ -20,16 +20,14 @@ use std::fmt::Write;
use std::ptr;
use std::str::FromStr;
use std::sync::RwLock;
use std::time::{Duration, SystemTime};
use libc::uintptr_t;
use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
use deltachat::chat::ChatId;
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::stock::StockMessage;
use deltachat::*;
@@ -59,7 +57,6 @@ use self::string::*;
/// and protected by an [RwLock]. Other than that it needs to store
/// the data which is passed into [dc_context_new].
pub struct ContextWrapper {
cb: Option<dc_callback_t>,
userdata: *mut libc::c_void,
os_name: String,
inner: RwLock<Option<context::Context>>,
@@ -85,12 +82,18 @@ pub type dc_callback_t =
pub type dc_context_t = ContextWrapper;
impl ContextWrapper {
/// Log a warning on the FFI context.
/// Log an error on the FFI context.
///
/// Like [error] but logs as a warning which only goes to the
/// logfile rather than being shown directly to the user.
unsafe fn warning(&self, msg: &str) {
self.translate_cb(Event::Warning(msg.to_string()));
/// As soon as a [ContextWrapper] exist it can be used to log an
/// error using the callback, even before [dc_context_open] is
/// called and an actual [Context] exists.
///
/// This function makes it easy to log an error.
unsafe fn error(&self, msg: &str) {
self.with_inner(|ctx| {
ctx.call_cb(Event::Error(msg.to_string()));
})
.unwrap();
}
/// Unlock the context and execute a closure with it.
@@ -108,113 +111,27 @@ impl ContextWrapper {
/// the appropriate return value for an error return since this
/// differs for various functions on the FFI API: sometimes 0,
/// NULL, an empty string etc.
unsafe fn with_inner<T, F>(&self, ctxfn: F) -> Result<T, ()>
fn with_inner<T, F>(&self, ctxfn: F) -> Result<T, ()>
where
F: FnOnce(&Context) -> T,
{
self.try_inner(|ctx| Ok(ctxfn(ctx))).map_err(|err| {
self.warning(&err.to_string());
})
}
/// Unlock the context and execute a closure with it.
///
/// This is like [ContextWrapper::with_inner] but uses
/// [failure::Error] as error type. This allows you to write a
/// closure which could produce many errors, use the `?` operator
/// to return them and handle them all as the return of this call.
fn try_inner<T, F>(&self, ctxfn: F) -> Result<T, failure::Error>
where
F: FnOnce(&Context) -> Result<T, failure::Error>,
{
let guard = self.inner.read().unwrap();
match guard.as_ref() {
Some(ref ctx) => ctxfn(ctx),
None => Err(failure::err_msg("context not open")),
Some(ref ctx) => Ok(ctxfn(ctx)),
None => {
eprintln!("context not open");
Err(())
}
}
}
/// Translates the callback from the rust style to the C-style version.
unsafe fn translate_cb(&self, event: Event) {
if let Some(ffi_cb) = self.cb {
let event_id = event.as_id();
match event {
Event::Info(msg)
| Event::SmtpConnected(msg)
| Event::ImapConnected(msg)
| Event::SmtpMessageSent(msg)
| Event::ImapMessageDeleted(msg)
| Event::ImapMessageMoved(msg)
| Event::ImapFolderEmptied(msg)
| Event::NewBlobFile(msg)
| Event::DeletedBlobFile(msg)
| Event::Warning(msg)
| Event::Error(msg)
| Event::ErrorNetwork(msg)
| Event::ErrorSelfNotInGroup(msg) => {
let data2 = CString::new(msg).unwrap_or_default();
ffi_cb(self, event_id, 0, data2.as_ptr() as uintptr_t);
}
Event::MsgsChanged { chat_id, msg_id }
| Event::IncomingMsg { chat_id, msg_id }
| Event::MsgDelivered { chat_id, msg_id }
| Event::MsgFailed { chat_id, msg_id }
| Event::MsgRead { chat_id, msg_id } => {
ffi_cb(
self,
event_id,
chat_id.to_u32() as uintptr_t,
msg_id.to_u32() as uintptr_t,
);
}
Event::ChatModified(chat_id) => {
ffi_cb(self, event_id, chat_id.to_u32() as uintptr_t, 0);
}
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
let id = id.unwrap_or_default();
ffi_cb(self, event_id, id as uintptr_t, 0);
}
Event::ConfigureProgress(progress) | Event::ImexProgress(progress) => {
ffi_cb(self, event_id, progress as uintptr_t, 0);
}
Event::ImexFileWritten(file) => {
let data1 = file.to_c_string().unwrap_or_default();
ffi_cb(self, event_id, data1.as_ptr() as uintptr_t, 0);
}
Event::SecurejoinInviterProgress {
contact_id,
progress,
}
| Event::SecurejoinJoinerProgress {
contact_id,
progress,
} => {
ffi_cb(
self,
event_id,
contact_id as uintptr_t,
progress as uintptr_t,
);
}
Event::SecurejoinMemberAdded {
chat_id,
contact_id,
} => {
ffi_cb(
self,
event_id,
chat_id.to_u32() as uintptr_t,
contact_id as uintptr_t,
);
}
}
}
fn is_open(&self) -> bool {
self.inner.read().unwrap().is_some()
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_new(
cb: Option<dc_callback_t>,
userdata: *mut libc::c_void,
os_name: *const libc::c_char,
) -> *mut dc_context_t {
@@ -226,11 +143,11 @@ pub unsafe extern "C" fn dc_context_new(
to_string_lossy(os_name)
};
let ffi_ctx = ContextWrapper {
cb,
userdata,
os_name,
inner: RwLock::new(None),
};
Box::into_raw(Box::new(ffi_ctx))
}
@@ -268,17 +185,11 @@ pub unsafe extern "C" fn dc_open(
return 0;
}
let ffi_context = &*context;
let rust_cb = move |_ctx: &Context, evt: Event| ffi_context.translate_cb(evt);
let ctx = if blobdir.is_null() || *blobdir == 0 {
Context::new(
Box::new(rust_cb),
ffi_context.os_name.clone(),
as_path(dbfile).to_path_buf(),
)
Context::new(ffi_context.os_name.clone(), as_path(dbfile).to_path_buf())
} else {
Context::with_blobdir(
Box::new(rust_cb),
ffi_context.os_name.clone(),
as_path(dbfile).to_path_buf(),
as_path(blobdir).to_path_buf(),
@@ -311,11 +222,7 @@ pub unsafe extern "C" fn dc_is_open(context: *mut dc_context_t) -> libc::c_int {
return 0;
}
let ffi_context = &*context;
let inner_guard = ffi_context.inner.read().unwrap();
match *inner_guard {
Some(_) => 1,
None => 0,
}
ffi_context.is_open() as libc::c_int
}
#[no_mangle]
@@ -351,7 +258,7 @@ pub unsafe extern "C" fn dc_set_config(
})
.unwrap_or(0),
Err(_) => {
ffi_context.warning("dc_set_config(): invalid key");
ffi_context.error("dc_set_config(): invalid key");
0
}
}
@@ -372,7 +279,7 @@ pub unsafe extern "C" fn dc_get_config(
.with_inner(|ctx| ctx.get_config(key).unwrap_or_default().strdup())
.unwrap_or_else(|_| "".strdup()),
Err(_) => {
ffi_context.warning("dc_get_config(): invalid key");
ffi_context.error("dc_get_config(): invalid key");
"".strdup()
}
}
@@ -407,28 +314,6 @@ pub unsafe extern "C" fn dc_set_stock_translation(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_config_from_qr(
context: *mut dc_context_t,
qr: *mut libc::c_char,
) -> libc::c_int {
if context.is_null() || qr.is_null() {
eprintln!("ignoring careless call to dc_set_config_from_qr");
return 0;
}
let qr = to_string_lossy(qr);
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| match qr::set_config_from_qr(ctx, &qr) {
Ok(()) => 1,
Err(err) => {
error!(ctx, "Failed to create account from QR code: {}", err);
0
}
})
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
@@ -504,183 +389,115 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_imap_jobs(context: *mut dc_context_t) {
pub unsafe extern "C" fn dc_context_run(context: *mut dc_context_t, cb: Option<dc_callback_t>) {
eprintln!("dc_context_run");
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_imap_jobs()");
eprintln!("ignoring careless call to dc_run()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_inbox_jobs(ctx))
.with_inner(|ctx| {
eprintln!("RUN");
ctx.run(|_ctx, event| {
translate_cb(ffi_context, cb, event);
});
eprintln!("RUN DONE");
})
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_imap_fetch(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_imap_fetch()");
return;
/// Translates the callback from the rust style to the C-style version.
unsafe fn translate_cb(ctx: &ContextWrapper, ffi_cb: Option<dc_callback_t>, event: Event) {
if let Some(ffi_cb) = ffi_cb {
let event_id = event.as_id();
match event {
Event::Info(msg)
| Event::SmtpConnected(msg)
| Event::ImapConnected(msg)
| Event::SmtpMessageSent(msg)
| Event::ImapMessageDeleted(msg)
| Event::ImapMessageMoved(msg)
| Event::ImapFolderEmptied(msg)
| Event::NewBlobFile(msg)
| Event::DeletedBlobFile(msg)
| Event::Warning(msg)
| Event::Error(msg)
| Event::ErrorNetwork(msg)
| Event::ErrorSelfNotInGroup(msg) => {
let data2 = CString::new(msg).unwrap_or_default();
ffi_cb(ctx, event_id, 0, data2.as_ptr() as uintptr_t);
}
Event::MsgsChanged { chat_id, msg_id }
| Event::IncomingMsg { chat_id, msg_id }
| Event::MsgDelivered { chat_id, msg_id }
| Event::MsgFailed { chat_id, msg_id }
| Event::MsgRead { chat_id, msg_id } => {
ffi_cb(
ctx,
event_id,
chat_id.to_u32() as uintptr_t,
msg_id.to_u32() as uintptr_t,
);
}
Event::ChatModified(chat_id) => {
ffi_cb(ctx, event_id, chat_id.to_u32() as uintptr_t, 0);
}
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
let id = id.unwrap_or_default();
ffi_cb(ctx, event_id, id as uintptr_t, 0);
}
Event::ConfigureProgress(progress) | Event::ImexProgress(progress) => {
ffi_cb(ctx, event_id, progress as uintptr_t, 0);
}
Event::ImexFileWritten(file) => {
let data1 = file.to_c_string().unwrap_or_default();
ffi_cb(ctx, event_id, data1.as_ptr() as uintptr_t, 0);
}
Event::SecurejoinInviterProgress {
contact_id,
progress,
}
| Event::SecurejoinJoinerProgress {
contact_id,
progress,
} => {
ffi_cb(
ctx,
event_id,
contact_id as uintptr_t,
progress as uintptr_t,
);
}
Event::SecurejoinMemberAdded {
chat_id,
contact_id,
} => {
ffi_cb(
ctx,
event_id,
chat_id.to_u32() as uintptr_t,
contact_id as uintptr_t,
);
}
}
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_inbox_fetch(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_imap_idle(context: *mut dc_context_t) {
// TODO rename function in co-ordination with UIs
pub unsafe extern "C" fn dc_context_shutdown(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_imap_idle()");
eprintln!("ignoring careless call to dc_shutdown()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_inbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_imap_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_imap_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_inbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_mvbox_fetch(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_mvbox_fetch()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_mvbox_fetch(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_mvbox_jobs(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_mvbox_jobs()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_mvbox_jobs(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_mvbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_mvbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_mvbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_mvbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_mvbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_mvbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_sentbox_fetch(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_sentbox_fetch()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_sentbox_fetch(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_sentbox_jobs(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_sentbox_jobs()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_sentbox_jobs(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_sentbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_sentbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_sentbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_sentbox_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_sentbox_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_sentbox_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_smtp_jobs(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_smtp_jobs()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_smtp_jobs(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_perform_smtp_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_perform_smtp_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::perform_smtp_idle(ctx))
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_interrupt_smtp_idle(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_interrupt_smtp_idle()");
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| job::interrupt_smtp_idle(ctx))
.with_inner(|ctx| {
eprintln!("SHUTDOWN");
ctx.shutdown();
eprintln!("SHUTDOWN:DONE")
})
.unwrap_or(())
}
@@ -696,35 +513,6 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_preconfigure_keypair(
context: *mut dc_context_t,
addr: *const libc::c_char,
public_data: *const libc::c_char,
secret_data: *const libc::c_char,
) -> i32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_preconfigure_keypair()");
return 0;
}
let ffi_context = &*context;
ffi_context
.try_inner(|ctx| {
let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?;
let public = key::SignedPublicKey::from_base64(&to_string_lossy(public_data))?;
let secret = key::SignedSecretKey::from_base64(&to_string_lossy(secret_data))?;
let keypair = key::KeyPair {
addr,
public,
secret,
};
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default)?;
Ok(1)
})
.log_err(ffi_context, "Failed to save keypair")
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_chatlist(
context: *mut dc_context_t,
@@ -768,7 +556,7 @@ pub unsafe extern "C" fn dc_create_chat_by_msg_id(context: *mut dc_context_t, ms
ffi_context
.with_inner(|ctx| {
chat::create_by_msg_id(ctx, MsgId::new(msg_id))
.log_err(ffi_context, "Failed to create chat from msg_id")
.log_err(ctx, "Failed to create chat from msg_id")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -788,7 +576,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
ffi_context
.with_inner(|ctx| {
chat::create_by_contact_id(ctx, contact_id)
.log_err(ffi_context, "Failed to create chat from contact_id")
.log_err(ctx, "Failed to create chat from contact_id")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -808,7 +596,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
ffi_context
.with_inner(|ctx| {
chat::get_by_contact_id(ctx, contact_id)
.log_err(ffi_context, "Failed to get chat for contact_id")
.log_err(ctx, "Failed to get chat for contact_id")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -1077,7 +865,7 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
ffi_context
.with_inner(|ctx| {
chat::marknoticed_chat(ctx, ChatId::new(chat_id))
.log_err(ffi_context, "Failed marknoticed chat")
.log_err(ctx, "Failed marknoticed chat")
.unwrap_or(())
})
.unwrap_or(())
@@ -1093,7 +881,7 @@ pub unsafe extern "C" fn dc_marknoticed_all_chats(context: *mut dc_context_t) {
ffi_context
.with_inner(|ctx| {
chat::marknoticed_all_chats(ctx)
.log_err(ffi_context, "Failed marknoticed all chats")
.log_err(ctx, "Failed marknoticed all chats")
.unwrap_or(())
})
.unwrap_or(())
@@ -1186,32 +974,28 @@ pub unsafe extern "C" fn dc_get_next_media(
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_visibility(
pub unsafe extern "C" fn dc_archive_chat(
context: *mut dc_context_t,
chat_id: u32,
archive: libc::c_int,
) {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_visibility()");
eprintln!("ignoring careless call to dc_archive_chat()");
return;
}
let ffi_context = &*context;
let visibility = match archive {
0 => ChatVisibility::Normal,
1 => ChatVisibility::Archived,
2 => ChatVisibility::Pinned,
_ => {
ffi_context.warning(
"ignoring careless call to dc_set_chat_visibility(): unknown archived state",
);
return;
}
let archive = if archive == 0 {
false
} else if archive == 1 {
true
} else {
return;
};
ffi_context
.with_inner(|ctx| {
ChatId::new(chat_id)
.set_visibility(ctx, visibility)
.log_err(ffi_context, "Failed setting chat visibility")
.set_archived(ctx, archive)
.log_err(ctx, "Failed archive chat")
.unwrap_or(())
})
.unwrap_or(())
@@ -1228,7 +1012,7 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
.with_inner(|ctx| {
ChatId::new(chat_id)
.delete(ctx)
.log_err(ffi_context, "Failed chat delete")
.log_err(ctx, "Failed chat delete")
.unwrap_or(())
})
.unwrap_or(())
@@ -1315,7 +1099,7 @@ pub unsafe extern "C" fn dc_create_group_chat(
ffi_context
.with_inner(|ctx| {
chat::create_group_chat(ctx, verified, to_string_lossy(name))
.log_err(ffi_context, "Failed to create group chat")
.log_err(ctx, "Failed to create group chat")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -1417,37 +1201,6 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_mute_duration(
context: *mut dc_context_t,
chat_id: u32,
duration: i64,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_mute_duration()");
return 0;
}
let ffi_context = &*context;
let muteDuration = match duration {
0 => MuteDuration::NotMuted,
-1 => MuteDuration::Forever,
n if n > 0 => MuteDuration::Until(SystemTime::now() + Duration::from_secs(duration as u64)),
_ => {
ffi_context.warning(
"dc_chat_set_mute_duration(): Can not use negative duration other than -1",
);
return 0;
}
};
ffi_context
.with_inner(|ctx| {
chat::set_muted(ctx, ChatId::new(chat_id), muteDuration)
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set mute duration")
})
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_info(
context: *mut dc_context_t,
@@ -1850,9 +1603,7 @@ pub unsafe extern "C" fn dc_imex_has_backup(
.with_inner(|ctx| match imex::has_backup(ctx, to_string_lossy(dir)) {
Ok(res) => res.strdup(),
Err(err) => {
// do not bubble up error to the user,
// the ui will expect that the file does not exist or cannot be accessed
warn!(ctx, "dc_imex_has_backup: {}", err);
error!(ctx, "dc_imex_has_backup: {}", err);
ptr::null_mut()
}
})
@@ -2054,9 +1805,7 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
location::delete_all(ctx).log_err(ffi_context, "Failed to delete locations")
})
.with_inner(|ctx| location::delete_all(ctx).log_err(ctx, "Failed to delete locations"))
.ok();
}
@@ -2455,17 +2204,13 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 {
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_visibility(chat: *mut dc_chat_t) -> libc::c_int {
pub unsafe extern "C" fn dc_chat_get_archived(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_get_visibility()");
eprintln!("ignoring careless call to dc_chat_get_archived()");
return 0;
}
let ffi_chat = &*chat;
match ffi_chat.chat.visibility {
ChatVisibility::Normal => 0,
ChatVisibility::Archived => 1,
ChatVisibility::Pinned => 2,
}
ffi_chat.chat.is_archived() as libc::c_int
}
#[no_mangle]
@@ -2528,37 +2273,6 @@ pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> l
ffi_chat.chat.is_sending_locations() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_muted(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_muted()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_muted() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_remaining_mute_duration(chat: *mut dc_chat_t) -> i64 {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_get_remaining_mute_duration()");
return 0;
}
let ffi_chat = &*chat;
if !ffi_chat.chat.is_muted() {
return 0;
}
// If the chat was muted to before the epoch, it is not muted.
match ffi_chat.chat.mute_duration {
MuteDuration::NotMuted => 0,
MuteDuration::Forever => -1,
MuteDuration::Until(when) => when
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_info_json(
context: *mut dc_context_t,
@@ -3227,7 +2941,7 @@ pub unsafe extern "C" fn dc_lot_get_text1(lot: *mut dc_lot_t) -> *mut libc::c_ch
}
let lot = &*lot;
lot.get_text1().strdup()
strdup_opt(lot.get_text1())
}
#[no_mangle]
@@ -3238,7 +2952,7 @@ pub unsafe extern "C" fn dc_lot_get_text2(lot: *mut dc_lot_t) -> *mut libc::c_ch
}
let lot = &*lot;
lot.get_text2().strdup()
strdup_opt(lot.get_text2())
}
#[no_mangle]
@@ -3290,16 +3004,11 @@ pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
libc::free(s as *mut _)
}
trait ResultExt<T, E> {
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T;
pub mod providers;
/// Log a warning to a [ContextWrapper] for an [Err] result.
///
/// Does nothing for an [Ok].
///
/// You can do this as soon as the wrapper exists, it does not
/// have to be open (which is required for the `warn!()` macro).
fn log_err(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E>;
pub trait ResultExt<T, E> {
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T;
fn log_err(self, context: &context::Context, message: &str) -> Result<T, E>;
}
impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
@@ -3313,17 +3022,22 @@ impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
}
}
fn log_err(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E> {
fn log_err(self, context: &context::Context, message: &str) -> Result<T, E> {
self.map_err(|err| {
unsafe {
wrapper.warning(&format!("{}: {}", message, err));
}
warn!(context, "{}: {}", message, err);
err
})
}
}
trait ResultNullableExt<T> {
unsafe fn strdup_opt(s: Option<impl AsRef<str>>) -> *mut libc::c_char {
match s {
Some(s) => s.as_ref().strdup(),
None => ptr::null_mut(),
}
}
pub trait ResultNullableExt<T> {
fn into_raw(self) -> *mut T;
}
@@ -3346,68 +3060,3 @@ fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> V
msg_ids
}
// dc_provider_t
#[no_mangle]
pub type dc_provider_t = provider::Provider;
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email(
context: *const dc_context_t,
addr: *const libc::c_char,
) -> *const dc_provider_t {
if context.is_null() || addr.is_null() {
eprintln!("ignoring careless call to dc_provider_new_from_email()");
return ptr::null();
}
let addr = to_string_lossy(addr);
match provider::get_provider_info(addr.as_str()) {
Some(provider) => provider,
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_overview_page(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_overview_page()");
return "".strdup();
}
let provider = &*provider;
provider.overview_page.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_before_login_hint(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_before_login_hint()");
return "".strdup();
}
let provider = &*provider;
provider.before_login_hint.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> libc::c_int {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_status()");
return 0;
}
let provider = &*provider;
provider.status as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_unref()");
return;
}
// currently, there is nothing to free, the provider info is a static object.
// this may change once we start localizing string.
}

View File

@@ -0,0 +1,91 @@
extern crate deltachat_provider_database;
use std::ptr;
use crate::string::{to_string_lossy, StrExt};
use deltachat_provider_database::StatusState;
#[no_mangle]
pub type dc_provider_t = deltachat_provider_database::Provider;
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_domain(
domain: *const libc::c_char,
) -> *const dc_provider_t {
match deltachat_provider_database::get_provider_info(&to_string_lossy(domain)) {
Some(provider) => provider,
None => ptr::null(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email(
email: *const libc::c_char,
) -> *const dc_provider_t {
let email = to_string_lossy(email);
let domain = deltachat_provider_database::get_domain_from_email(&email);
match deltachat_provider_database::get_provider_info(domain) {
Some(provider) => provider,
None => ptr::null(),
}
}
macro_rules! null_guard {
($context:tt) => {
if $context.is_null() {
return ptr::null_mut() as *mut libc::c_char;
}
};
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_overview_page(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
format!(
"{}/{}",
deltachat_provider_database::PROVIDER_OVERVIEW_URL,
(*provider).overview_page
)
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_name(provider: *const dc_provider_t) -> *mut libc::c_char {
null_guard!(provider);
(*provider).name.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_markdown(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
(*provider).markdown.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status_date(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
(*provider).status.date.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> u32 {
if provider.is_null() {
return 0;
}
match (*provider).status.state {
StatusState::OK => 1,
StatusState::PREPARATION => 2,
StatusState::BROKEN => 3,
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_unref(_provider: *const dc_provider_t) {}
// TODO expose general provider overview url?

View File

@@ -1,6 +1,5 @@
use failure::Fail;
use std::ffi::{CStr, CString};
use std::ptr;
/// Duplicates a string
///
@@ -145,7 +144,7 @@ pub trait CStringExt {
impl CStringExt for CString {}
/// Convenience methods to turn strings into C strings.
/// Convenience methods to make transitioning from raw C strings easier.
///
/// To interact with (legacy) C APIs we often need to convert from
/// Rust strings to raw C strings. This can be clumsy to do correctly
@@ -175,35 +174,6 @@ impl<T: AsRef<str>> StrExt for T {
}
}
/// Convenience methods to turn optional strings into C strings.
///
/// This is the same as the [StrExt] trait but a different trait name
/// to work around the type system not allowing to implement [StrExt]
/// for `Option<impl StrExt>` When we already have an [StrExt] impl
/// for `AsRef<&str>`.
///
/// When the [Option] is [Option::Some] this behaves just like
/// [StrExt::strdup], when it is [Option::None] a null pointer is
/// returned.
pub trait OptStrExt {
/// Allocate a new raw C `*char` version of this string, or NULL.
///
/// See [StrExt::strdup] for details.
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> OptStrExt for Option<T> {
unsafe fn strdup(&self) -> *mut libc::c_char {
match self {
Some(s) => {
let tmp = CString::yolo(s.as_ref());
dc_strdup(tmp.as_ptr())
}
None => ptr::null_mut(),
}
}
}
pub fn to_string_lossy(s: *const libc::c_char) -> String {
if s.is_null() {
return "".into();
@@ -377,19 +347,4 @@ mod tests {
assert_eq!(cmp, 0);
}
}
#[test]
fn test_strdup_opt_string() {
unsafe {
let s = Some("hello");
let c = s.strdup();
let cmp = strcmp(c, b"hello\x00" as *const u8 as *const libc::c_char);
free(c as *mut libc::c_void);
assert_eq!(cmp, 0);
let s: Option<&str> = None;
let c = s.strdup();
assert_eq!(c, ptr::null_mut());
}
}
}

View File

@@ -1,8 +1,9 @@
use std::path::Path;
use std::str::FromStr;
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
use deltachat::chat::{self, Chat, ChatId};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
@@ -18,7 +19,6 @@ use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::Event;
use deltachat::{config, provider};
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
@@ -94,7 +94,7 @@ fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), Error> {
let data = dc_read_file(context, filename)?;
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false) {
if let Err(err) = dc_receive_imf(context, &data, "import", 0, 0) {
println!("dc_receive_imf errored: {:?}", err);
}
Ok(())
@@ -371,8 +371,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
listmedia\n\
archive <chat-id>\n\
unarchive <chat-id>\n\
pin <chat-id>\n\
unpin <chat-id>\n\
delchat <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
@@ -394,7 +392,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
getqr [<chat-id>]\n\
getbadqr\n\
checkqr <qr-content>\n\
providerinfo <addr>\n\
event <event-id to test>\n\
fileinfo <file>\n\
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
@@ -513,19 +510,14 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
println!(
"{}#{}: {} [{} fresh] {}",
"{}#{}: {} [{} fresh]",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
chat.get_id().get_fresh_msg_cnt(context),
match chat.visibility {
ChatVisibility::Normal => "",
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
);
let lot = chatlist.get_summary(context, i, Some(&chat));
let statestr = if chat.visibility == ChatVisibility::Archived {
let statestr = if chat.is_archived() {
" [Archived]"
} else {
match lot.get_state() {
@@ -849,18 +841,10 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
}
print!("\n");
}
"archive" | "unarchive" | "pin" | "unpin" => {
"archive" | "unarchive" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id.set_visibility(
context,
match arg0 {
"archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned,
_ => panic!("Unexpected command (This should never happen)"),
},
)?;
chat_id.set_archived(context, arg0 == "archive")?;
}
"delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
@@ -982,31 +966,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
res.get_text2()
);
}
"setqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(context, arg1) {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => println!("Cannot set config from QR code: {:?}", err),
}
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1) {
Some(info) => {
println!("Information for provider belonging to {}:", arg1);
println!("status: {}", info.status as u32);
println!("before_login_hint: {}", info.before_login_hint);
println!("after_login_hint: {}", info.after_login_hint);
println!("overview_page: {}", info.overview_page);
for server in info.server.iter() {
println!("server: {}:{}", server.hostname, server.port,);
}
}
None => {
println!("No information for provider belonging to {} found.", arg1);
}
}
}
// TODO: implement this again, unclear how to match this through though, without writing a parser.
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");

View File

@@ -9,21 +9,17 @@ extern crate deltachat;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate rusqlite;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::sync::{Arc, RwLock};
use deltachat::chat::ChatId;
use deltachat::config;
use deltachat::context::*;
use deltachat::job::*;
use deltachat::oauth2::*;
use deltachat::securejoin::*;
use deltachat::Event;
@@ -113,107 +109,6 @@ fn receive_event(_context: &Context, event: Event) {
}
}
// Threads for waiting for messages and for jobs
lazy_static! {
static ref HANDLE: Arc<Mutex<Option<Handle>>> = Arc::new(Mutex::new(None));
static ref IS_RUNNING: AtomicBool = AtomicBool::new(true);
}
struct Handle {
handle_imap: Option<std::thread::JoinHandle<()>>,
handle_mvbox: Option<std::thread::JoinHandle<()>>,
handle_sentbox: Option<std::thread::JoinHandle<()>>,
handle_smtp: Option<std::thread::JoinHandle<()>>,
}
macro_rules! while_running {
($code:block) => {
if IS_RUNNING.load(Ordering::Relaxed) {
$code
} else {
break;
}
};
}
fn start_threads(c: Arc<RwLock<Context>>) {
if HANDLE.clone().lock().unwrap().is_some() {
return;
}
println!("Starting threads");
IS_RUNNING.store(true, Ordering::Relaxed);
let ctx = c.clone();
let handle_imap = std::thread::spawn(move || loop {
while_running!({
perform_inbox_jobs(&ctx.read().unwrap());
perform_inbox_fetch(&ctx.read().unwrap());
while_running!({
let context = ctx.read().unwrap();
perform_inbox_idle(&context);
});
});
});
let ctx = c.clone();
let handle_mvbox = std::thread::spawn(move || loop {
while_running!({
perform_mvbox_fetch(&ctx.read().unwrap());
while_running!({
perform_mvbox_idle(&ctx.read().unwrap());
});
});
});
let ctx = c.clone();
let handle_sentbox = std::thread::spawn(move || loop {
while_running!({
perform_sentbox_fetch(&ctx.read().unwrap());
while_running!({
perform_sentbox_idle(&ctx.read().unwrap());
});
});
});
let ctx = c;
let handle_smtp = std::thread::spawn(move || loop {
while_running!({
perform_smtp_jobs(&ctx.read().unwrap());
while_running!({
perform_smtp_idle(&ctx.read().unwrap());
});
});
});
*HANDLE.clone().lock().unwrap() = Some(Handle {
handle_imap: Some(handle_imap),
handle_mvbox: Some(handle_mvbox),
handle_sentbox: Some(handle_sentbox),
handle_smtp: Some(handle_smtp),
});
}
fn stop_threads(context: &Context) {
if let Some(ref mut handle) = *HANDLE.clone().lock().unwrap() {
println!("Stopping threads");
IS_RUNNING.store(false, Ordering::Relaxed);
interrupt_inbox_idle(context);
interrupt_mvbox_idle(context);
interrupt_sentbox_idle(context);
interrupt_smtp_idle(context);
handle.handle_imap.take().unwrap().join().unwrap();
handle.handle_mvbox.take().unwrap().join().unwrap();
handle.handle_sentbox.take().unwrap().join().unwrap();
handle.handle_smtp.take().unwrap().join().unwrap();
}
}
// === The main loop
struct DcHelper {
completer: FilenameCompleter,
highlighter: MatchingBracketHighlighter,
@@ -262,7 +157,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 26] = [
const CHAT_COMMANDS: [&str; 24] = [
"listchats",
"listarchived",
"chat",
@@ -286,8 +181,6 @@ const CHAT_COMMANDS: [&str; 26] = [
"listmedia",
"archive",
"unarchive",
"pin",
"unpin",
"delchat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
@@ -421,9 +314,7 @@ fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
}
rl.save_history(".dc-history.txt")?;
println!("history saved");
{
stop_threads(&ctx.read().unwrap());
}
ctx.read().unwrap().shutdown();
Ok(())
}
@@ -441,27 +332,19 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
match arg0 {
"connect" => {
start_threads(ctx);
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
}
"disconnect" => {
stop_threads(&ctx.read().unwrap());
}
"smtp-jobs" => {
if HANDLE.clone().lock().unwrap().is_some() {
println!("smtp-jobs are already running in a thread.",);
} else {
perform_smtp_jobs(&ctx.read().unwrap());
}
}
"imap-jobs" => {
if HANDLE.clone().lock().unwrap().is_some() {
println!("inbox-jobs are already running in a thread.");
} else {
perform_inbox_jobs(&ctx.read().unwrap());
}
ctx.read().unwrap().shutdown();
}
"configure" => {
start_threads(ctx.clone());
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
ctx.read().unwrap().configure();
}
"oauth2" => {
@@ -485,7 +368,11 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
print!("\x1b[1;1H\x1b[2J");
}
"getqr" | "getbadqr" => {
start_threads(ctx.clone());
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
if let Some(mut qr) = dc_get_securejoin_qr(
&ctx.read().unwrap(),
ChatId::new(arg1.parse().unwrap_or_default()),
@@ -505,8 +392,12 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
}
}
"joinqr" => {
start_threads(ctx.clone());
if !arg0.is_empty() {
crossbeam::scope(|s| {
s.spawn(|_| ctx.read().unwrap().run());
})
.unwrap();
dc_join_securejoin(&ctx.read().unwrap(), arg1);
}
}

View File

@@ -1,6 +1,5 @@
extern crate deltachat;
use std::sync::{Arc, RwLock};
use std::{thread, time};
use tempfile::tempdir;
@@ -9,10 +8,6 @@ use deltachat::chatlist::*;
use deltachat::config;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::job::{
perform_inbox_fetch, perform_inbox_idle, perform_inbox_jobs, perform_smtp_idle,
perform_smtp_jobs,
};
use deltachat::Event;
fn cb(_ctx: &Context, event: Event) {
@@ -20,13 +15,13 @@ fn cb(_ctx: &Context, event: Event) {
match event {
Event::ConfigureProgress(progress) => {
println!(" progress: {}", progress);
print!(" progress: {}\n", progress);
}
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
println!(" {}", msg);
print!(" {}\n", msg);
}
_ => {
println!();
print!("\n");
}
}
}
@@ -35,77 +30,52 @@ fn main() {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
println!("creating database {:?}", dbfile);
let ctx =
Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context");
let running = Arc::new(RwLock::new(true));
let ctx = Context::new("FakeOs".into(), dbfile).expect("Failed to create context");
let info = ctx.get_info();
let duration = time::Duration::from_millis(4000);
let duration = time::Duration::from_millis(8000);
println!("info: {:#?}", info);
let ctx = Arc::new(ctx);
let ctx1 = ctx.clone();
let r1 = running.clone();
let t1 = thread::spawn(move || {
while *r1.read().unwrap() {
perform_inbox_jobs(&ctx1);
if *r1.read().unwrap() {
perform_inbox_fetch(&ctx1);
crossbeam::scope(|s| {
let t1 = s.spawn(|_| {
ctx.run(cb);
});
if *r1.read().unwrap() {
perform_inbox_idle(&ctx1);
}
}
println!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 2, "missing password");
let pw = args[1].clone();
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
ctx.configure();
thread::sleep(duration);
println!("sending a message");
let contact_id =
Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap();
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
println!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
for i in 0..chats.len() {
let summary = chats.get_summary(&ctx, 0, None);
let text1 = summary.get_text1();
let text2 = summary.get_text2();
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
}
});
let ctx1 = ctx.clone();
let r1 = running.clone();
let t2 = thread::spawn(move || {
while *r1.read().unwrap() {
perform_smtp_jobs(&ctx1);
if *r1.read().unwrap() {
perform_smtp_idle(&ctx1);
}
}
});
thread::sleep(duration);
println!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 2, "missing password");
let pw = args[1].clone();
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
ctx.configure();
println!("stopping threads");
ctx.shutdown();
thread::sleep(duration);
println!("joining");
t1.join().unwrap();
println!("sending a message");
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap();
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
println!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
for i in 0..chats.len() {
let summary = chats.get_summary(&ctx, 0, None);
let text1 = summary.get_text1();
let text2 = summary.get_text2();
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
}
thread::sleep(duration);
println!("stopping threads");
*running.write().unwrap() = false;
deltachat::job::interrupt_inbox_idle(&ctx);
deltachat::job::interrupt_smtp_idle(&ctx);
println!("joining");
t1.join().unwrap();
t2.join().unwrap();
println!("closing");
println!("closing");
})
.unwrap();
}

View File

@@ -1,10 +1,3 @@
0.800.0
-------
- use latest core 1.25.0
- refine tests and some internal changes to core bindings
0.700.0
---------

View File

@@ -52,21 +52,20 @@ Python packages before running the tests::
pytest -v tests
running "live" tests with temporary accounts
---------------------------------------------
running "live" tests (experimental)
-----------------------------------
If you want to run "liveconfig" functional tests you can set
``DCC_NEW_TMP_EMAIL`` to:
``DCC_PY_LIVECONFIG`` to:
- a particular https-url that you can ask for from the delta
chat devs. This is implemented on the server side via
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
chat devs.
- or the path of a file that contains two lines, each describing
via "addr=... mail_pw=..." a test account login that will
be used for the live tests.
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real
e-mail accounts and run through all functional "liveconfig" tests.
@@ -130,7 +129,7 @@ organization::
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e DCC_NEW_TMP_EMAIL \
$ docker run -e DCC_PY_LIVECONFIG \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh

View File

@@ -15,7 +15,3 @@ div.globaltoc {
img.logo {
height: 120px;
}
div.footer {
display: none;
}

View File

@@ -55,7 +55,7 @@ master_doc = 'index'
# General information about the project.
project = u'deltachat'
copyright = u'2020, holger krekel and contributors'
copyright = u'2018, holger krekel and contributors'
# The language for content autogenerated by Sphinx. Refer to documentation

View File

@@ -1,14 +1,15 @@
deltachat python bindings
=========================
The ``deltachat`` Python package provides two layers of bindings for the
core Rust-library of the https://delta.chat messaging ecosystem:
The ``deltachat`` Python package provides two bindings for the core Rust-library
of the https://delta.chat messaging ecosystem:
- :doc:`api` is a high level interface to deltachat-core which aims
to be memory safe and thoroughly tested through continous tox/pytest runs.
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
<https://github.com/deltachat/deltachat-core-rust>`_.
- :doc:`capi` is a lowlevel CFFI-binding to the previous
`deltachat-core C-API <https://c.delta.chat>`_ (so far the Rust library
replicates exactly the same C-level API).
@@ -27,6 +28,7 @@ getting started
links
changelog
api
capi
lapi
..

7
python/fail_test.py Normal file
View File

@@ -0,0 +1,7 @@
from __future__ import print_function
from deltachat import capi
from deltachat.capi import ffi, lib
if __name__ == "__main__":
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
lib.dc_context_shutdown(ctx)

View File

@@ -11,15 +11,18 @@ import sys
if __name__ == "__main__":
target = os.environ.get("DCC_RS_TARGET")
if target is None:
os.environ["DCC_RS_TARGET"] = target = "debug"
os.environ["DCC_RS_TARGET"] = target = "release"
if "DCC_RS_DEV" not in os.environ:
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.environ["DCC_RS_DEV"] = dn
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
if target == 'release':
cmd.append("--release")
subprocess.check_call(cmd)
# build the core library in release + debug mode because
# as of Nov 2019 rPGP generates RSA keys which take
# prohibitively long for non-release installs
os.environ["RUSTFLAGS"] = "-g"
subprocess.check_call([
"cargo", "build", "-p", "deltachat_ffi", "--" + target
])
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":

View File

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

View File

@@ -4,9 +4,13 @@ from __future__ import print_function
import atexit
import threading
import os
import re
import time
from array import array
from queue import Queue
try:
from queue import Queue, Empty
except ImportError:
from Queue import Queue, Empty
import deltachat
from . import const
@@ -15,8 +19,6 @@ from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
from .chat import Chat
from .message import Message
from .contact import Contact
from .eventlogger import EventLogger
from .hookspec import get_plugin_manager, hookimpl
class Account(object):
@@ -24,40 +26,34 @@ class Account(object):
by the underlying deltachat core library. All public Account methods are
meant to be memory-safe and return memory-safe objects.
"""
def __init__(self, db_path, logid=None, os_name=None, debug=True):
def __init__(self, db_path, logid=None, eventlogging=True, os_name=None, debug=True):
""" initialize account object.
:param db_path: a path to the account database. The database
will be created if it doesn't exist.
:param logid: an optional logging prefix that should be used with
the default internal logging.
:param eventlogging: if False no eventlogging and no context callback will be configured
:param os_name: this will be put to the X-Mailer header in outgoing messages
:param debug: turn on debug logging for events.
"""
self._dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
lib.dc_context_new(ffi.NULL, as_dc_charpointer(os_name)),
_destroy_dc_context,
)
self._evlogger = EventLogger(self, logid, debug)
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
if eventlogging:
self._evlogger = EventLogger(self._dc_context, logid, debug)
deltachat.set_context_callback(self._dc_context, self._process_event)
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
else:
self._threads = IOThreads(self._dc_context)
# register event call back and initialize plugin system
def _ll_event(ctx, evt_name, data1, data2):
assert ctx == self._dc_context
self.pluggy.hook.process_low_level_event(
account=self, event_name=evt_name, data1=data1, data2=data2
)
self.pluggy = get_plugin_manager()
self.pluggy.register(self._evlogger)
deltachat.set_context_callback(self._dc_context, _ll_event)
# open database
if hasattr(db_path, "encode"):
db_path = db_path.encode("utf8")
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
raise ValueError("Could not dc_open: {}".format(db_path))
self._configkeys = self.get_config("sys.config_keys").split()
self._imex_events = Queue()
atexit.register(self.shutdown)
# def __del__(self):
@@ -122,18 +118,6 @@ class Account(object):
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
return from_dc_charpointer(res)
def _preconfigure_keypair(self, addr, public, secret):
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
"""
res = lib.dc_preconfigure_keypair(self._dc_context,
as_dc_charpointer(addr),
as_dc_charpointer(public),
as_dc_charpointer(secret))
if res == 0:
raise Exception("Failed to set key")
def configure(self, **kwargs):
""" set config values and configure this account.
@@ -181,6 +165,11 @@ class Account(object):
raise ValueError("no flags set")
lib.dc_empty_server(self._dc_context, flags)
def get_infostring(self):
""" return info of the configured account. """
self.check_is_configured()
return from_dc_charpointer(lib.dc_get_info(self._dc_context))
def get_latest_backupfile(self, backupdir):
""" return the latest backup file in a given directory.
"""
@@ -381,12 +370,25 @@ class Account(object):
raise RuntimeError("found more than one new file")
return export_files[0]
def _imex_events_clear(self):
try:
while True:
self._imex_events.get_nowait()
except Empty:
pass
def _export(self, path, imex_cmd):
with ImexTracker(self) as imex_tracker:
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
return imex_tracker.wait_finish()
self._imex_events_clear()
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
files_written = []
while True:
ev = self._imex_events.get()
if isinstance(ev, str):
files_written.append(ev)
elif isinstance(ev, bool):
if not ev:
raise ValueError("export failed, exp-files: {}".format(files_written))
return files_written
def import_self_keys(self, path):
""" Import private keys found in the `path` directory.
@@ -404,11 +406,10 @@ class Account(object):
self._import(path, imex_cmd=12)
def _import(self, path, imex_cmd):
with ImexTracker(self) as imex_tracker:
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
imex_tracker.wait_finish()
self._imex_events_clear()
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._imex_events.get():
raise ValueError("import from path '{}' failed".format(path))
def initiate_key_transfer(self):
"""return setup code after a Autocrypt setup message
@@ -485,7 +486,7 @@ class Account(object):
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
return self.get_message_by_id(ev[2])
def start_threads(self, mvbox=False, sentbox=False):
def start_threads(self):
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
@@ -493,7 +494,7 @@ class Account(object):
"""
if not self.is_configured():
self.configure()
self._threads.start(mvbox=mvbox, sentbox=sentbox)
self._threads.start()
def stop_threads(self, wait=True):
""" stop IMAP/SMTP threads. """
@@ -511,7 +512,24 @@ class Account(object):
deltachat.clear_context_callback(self._dc_context)
del self._dc_context
atexit.unregister(self.shutdown)
self.pluggy.unregister(self._evlogger)
def _process_event(self, ctx, evt_name, data1, data2):
assert ctx == self._dc_context
if hasattr(self, "_evlogger"):
self._evlogger(evt_name, data1, data2)
method = getattr(self, "on_" + evt_name.lower(), None)
if method is not None:
method(data1, data2)
return 0
def on_dc_event_imex_progress(self, data1, data2):
if data1 == 1000:
self._imex_events.put(True)
elif data1 == 0:
self._imex_events.put(False)
def on_dc_event_imex_file_written(self, data1, data2):
self._imex_events.put(data1)
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
"""set a new location. It effects all chats where we currently
@@ -528,41 +546,6 @@ class Account(object):
raise ValueError("no chat is streaming locations")
class ImexTracker:
def __init__(self, account):
self._imex_events = Queue()
self.account = account
def __enter__(self):
self.account.pluggy.register(self)
return self
def __exit__(self, *args):
self.account.pluggy.unregister(self)
@hookimpl
def process_low_level_event(self, account, event_name, data1, data2):
# there could be multiple accounts instantiated
if self.account is not account:
return
if event_name == "DC_EVENT_IMEX_PROGRESS":
self._imex_events.put(data1)
elif event_name == "DC_EVENT_IMEX_FILE_WRITTEN":
self._imex_events.put(data1)
def wait_finish(self, progress_timeout=60):
""" Return list of written files, raise ValueError if ExportFailed. """
files_written = []
while True:
ev = self._imex_events.get(timeout=progress_timeout)
if isinstance(ev, str):
files_written.append(ev)
elif ev == 0:
raise ValueError("export failed, exp-files: {}".format(files_written))
elif ev == 1000:
return files_written
class IOThreads:
def __init__(self, dc_context, log_event=lambda *args: None):
self._dc_context = dc_context
@@ -573,16 +556,9 @@ class IOThreads:
def is_started(self):
return len(self._name2thread) > 0
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
def start(self):
assert not self.is_started()
if imap:
self._start_one_thread("inbox", self.imap_thread_run)
if mvbox:
self._start_one_thread("mvbox", self.mvbox_thread_run)
if sentbox:
self._start_one_thread("sentbox", self.sentbox_thread_run)
if smtp:
self._start_one_thread("smtp", self.smtp_thread_run)
self._start_one_thread("deltachat", self.dc_thread_run)
def _start_one_thread(self, name, func):
self._name2thread[name] = t = threading.Thread(target=func, name=name)
@@ -590,57 +566,91 @@ class IOThreads:
t.start()
def stop(self, wait=False):
self._thread_quitflag = True
# Workaround for a race condition. Make sure that thread is
# not in between checking for quitflag and entering idle.
time.sleep(0.5)
lib.dc_interrupt_imap_idle(self._dc_context)
lib.dc_interrupt_smtp_idle(self._dc_context)
lib.dc_interrupt_mvbox_idle(self._dc_context)
lib.dc_interrupt_sentbox_idle(self._dc_context)
lib.dc_context_shutdown(self._dc_context)
if wait:
for name, thread in self._name2thread.items():
thread.join()
def imap_thread_run(self):
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_imap_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_idle(self._dc_context)
def dc_thread_run(self):
self._log_event("py-bindings-info", 0, "DC THREAD START")
lib.dc_context_run(self._dc_context, lib.py_dc_callback)
self._log_event("py-bindings-info", 0, "INBOX THREAD FINISHED")
def mvbox_thread_run(self):
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_mvbox_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
def sentbox_thread_run(self):
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_sentbox_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
class EventLogger:
_loglock = threading.RLock()
def smtp_thread_run(self):
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
while not self._thread_quitflag:
lib.dc_perform_smtp_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_smtp_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
def __init__(self, dc_context, logid=None, debug=True):
self._dc_context = dc_context
self._event_queue = Queue()
self._debug = debug
if logid is None:
logid = str(self._dc_context).strip(">").split()[-1]
self.logid = logid
self._timeout = None
self.init_time = time.time()
def __call__(self, evt_name, data1, data2):
self._log_event(evt_name, data1, data2)
self._event_queue.put((evt_name, data1, data2))
def set_timeout(self, timeout):
self._timeout = timeout
def consume_events(self, check_error=True):
while not self._event_queue.empty():
self.get()
def get(self, timeout=None, check_error=True):
timeout = timeout or self._timeout
ev = self._event_queue.get(timeout=timeout)
if check_error and ev[0] == "DC_EVENT_ERROR":
raise ValueError("{}({!r},{!r})".format(*ev))
return ev
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile("(?:{}).*".format(event_name_regex))
while 1:
try:
ev = self._event_queue.get(False)
except Empty:
break
else:
assert not rex.match(ev[0]), "event found {}".format(ev)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
rex = re.compile("(?:{}).*".format(event_name_regex))
while 1:
ev = self.get(timeout=timeout, check_error=check_error)
if rex.match(ev[0]):
return ev
def get_info_matching(self, regex):
rex = re.compile("(?:{}).*".format(regex))
while 1:
ev = self.get_matching("DC_EVENT_INFO")
if rex.match(ev[2]):
return ev
def _log_event(self, evt_name, data1, data2):
# don't show events that are anyway empty impls now
if evt_name == "DC_EVENT_GET_STRING":
return
if self._debug:
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
self._log(evpart)
def _log(self, msg):
t = threading.currentThread()
tname = getattr(t, "name", t)
if tname == "MainThread":
tname = "MAIN"
with self._loglock:
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):

View File

@@ -58,13 +58,6 @@ class Chat(object):
"""
return self.id == const.DC_CHAT_ID_DEADDROP
def is_muted(self):
""" return true if this chat is muted.
:returns: True if chat is muted, False otherwise.
"""
return lib.dc_chat_is_muted(self._dc_chat)
def is_promoted(self):
""" return True if this chat is promoted, i.e.
the member contacts are aware of their membership,
@@ -91,43 +84,12 @@ class Chat(object):
def set_name(self, name):
""" set name of this chat.
:param name: as a unicode string.
:param: name as a unicode string.
:returns: None
"""
name = as_dc_charpointer(name)
return lib.dc_set_chat_name(self._dc_context, self.id, name)
def mute(self, duration=None):
""" mutes the chat
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
:returns: None
"""
if duration is None:
mute_duration = -1
else:
mute_duration = duration
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration)
if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self):
""" unmutes the chat
:returns: None
"""
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0)
if not bool(ret):
raise ValueError("Failed to unmute chat")
def get_mute_duration(self):
""" Returns the number of seconds until the mute of this chat is lifted.
:param duration:
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
"""
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
def get_type(self):
""" return type of this chat.
@@ -423,7 +385,7 @@ class Chat(object):
"""return True if this chat is archived.
:returns: True if archived.
"""
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
return lib.dc_chat_get_archived(self._dc_chat)
def enable_sending_locations(self, seconds):
"""enable sending locations for this chat.

View File

@@ -18,7 +18,6 @@ DC_QR_ASK_VERIFYGROUP = 202
DC_QR_FPR_OK = 210
DC_QR_FPR_MISMATCH = 220
DC_QR_FPR_WITHOUT_ADDR = 230
DC_QR_ACCOUNT = 250
DC_QR_ADDR = 320
DC_QR_TEXT = 330
DC_QR_URL = 332
@@ -103,15 +102,9 @@ DC_EVENT_FILE_COPIED = 2055
DC_EVENT_IS_OFFLINE = 2081
DC_EVENT_GET_STRING = 2091
DC_STR_SELFNOTINGRP = 21
DC_KEY_GEN_DEFAULT = 0
DC_KEY_GEN_RSA2048 = 1
DC_KEY_GEN_ED25519 = 2
DC_PROVIDER_STATUS_OK = 1
DC_PROVIDER_STATUS_PREPARATION = 2
DC_PROVIDER_STATUS_BROKEN = 3
DC_CHAT_VISIBILITY_NORMAL = 0
DC_CHAT_VISIBILITY_ARCHIVED = 1
DC_CHAT_VISIBILITY_PINNED = 2
DC_STR_NOMESSAGES = 1
DC_STR_SELF = 2
DC_STR_DRAFT = 3
@@ -164,7 +157,7 @@ DC_STR_COUNT = 68
def read_event_defines(f):
rex = re.compile(r'#define\s+((?:DC_EVENT|DC_QR|DC_MSG|DC_LP|DC_EMPTY|DC_CERTCK|DC_STATE|DC_STR|'
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER|DC_KEY_GEN)_\S+)\s+([x\d]+).*')
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER)_\S+)\s+([x\d]+).*')
for line in f:
m = rex.match(line)
if m:

View File

@@ -1,81 +0,0 @@
import threading
import re
import time
from queue import Queue, Empty
from .hookspec import hookimpl
class EventLogger:
_loglock = threading.RLock()
def __init__(self, account, logid=None, debug=True):
self.account = account
self._event_queue = Queue()
self._debug = debug
if logid is None:
logid = str(self.account._dc_context).strip(">").split()[-1]
self.logid = logid
self._timeout = None
self.init_time = time.time()
@hookimpl
def process_low_level_event(self, account, event_name, data1, data2):
if self.account == account:
self._log_event(event_name, data1, data2)
self._event_queue.put((event_name, data1, data2))
def set_timeout(self, timeout):
self._timeout = timeout
def consume_events(self, check_error=True):
while not self._event_queue.empty():
self.get(check_error=check_error)
def get(self, timeout=None, check_error=True):
timeout = timeout or self._timeout
ev = self._event_queue.get(timeout=timeout)
if check_error and ev[0] == "DC_EVENT_ERROR":
raise ValueError("{}({!r},{!r})".format(*ev))
return ev
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile("(?:{}).*".format(event_name_regex))
while 1:
try:
ev = self._event_queue.get(False)
except Empty:
break
else:
assert not rex.match(ev[0]), "event found {}".format(ev)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
rex = re.compile("(?:{}).*".format(event_name_regex))
while 1:
ev = self.get(timeout=timeout, check_error=check_error)
if rex.match(ev[0]):
return ev
def get_info_matching(self, regex):
rex = re.compile("(?:{}).*".format(regex))
while 1:
ev = self.get_matching("DC_EVENT_INFO")
if rex.match(ev[2]):
return ev
def _log_event(self, evt_name, data1, data2):
# don't show events that are anyway empty impls now
if evt_name == "DC_EVENT_GET_STRING":
return
if self._debug:
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
self._log(evpart)
def _log(self, msg):
t = threading.currentThread()
tname = getattr(t, "name", t)
if tname == "MainThread":
tname = "MAIN"
with self._loglock:
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))

View File

@@ -1,25 +0,0 @@
""" Hooks for python bindings """
import pluggy
name = "deltachat"
hookspec = pluggy.HookspecMarker(name)
hookimpl = pluggy.HookimplMarker(name)
_plugin_manager = None
def get_plugin_manager():
global _plugin_manager
if _plugin_manager is None:
_plugin_manager = pluggy.PluginManager(name)
_plugin_manager.add_hookspecs(DeltaChatHookSpecs)
return _plugin_manager
class DeltaChatHookSpecs:
""" Plugin Hook specifications for Python bindings to Delta Chat CFFI. """
@hookspec
def process_low_level_event(self, account, event_name, data1, data2):
""" process a CFFI low level events for a given account. """

View File

@@ -11,18 +11,27 @@ class ProviderNotFoundError(Exception):
class Provider(object):
"""Provider information.
:param domain: The email to get the provider info for.
:param domain: The domain to get the provider info for, this is
normally the part following the `@` of the domain.
"""
def __init__(self, account, addr):
def __init__(self, domain):
provider = ffi.gc(
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
lib.dc_provider_new_from_domain(as_dc_charpointer(domain)),
lib.dc_provider_unref,
)
if provider == ffi.NULL:
raise ProviderNotFoundError("Provider not found")
self._provider = provider
@classmethod
def from_email(cls, email):
"""Create provider info from an email address.
:param email: Email address to get provider info for.
"""
return cls(email.split('@')[-1])
@property
def overview_page(self):
"""URL to the overview page of the provider on providers.delta.chat."""
@@ -30,10 +39,21 @@ class Provider(object):
lib.dc_provider_get_overview_page(self._provider))
@property
def get_before_login_hints(self):
"""Should be shown to the user on login."""
def name(self):
"""The name of the provider."""
return from_dc_charpointer(lib.dc_provider_get_name(self._provider))
@property
def markdown(self):
"""Content of the information page, formatted as markdown."""
return from_dc_charpointer(
lib.dc_provider_get_before_login_hints(self._provider))
lib.dc_provider_get_markdown(self._provider))
@property
def status_date(self):
"""The date the provider info was last updated, as a string."""
return from_dc_charpointer(
lib.dc_provider_get_status_date(self._provider))
@property
def status(self):

View File

@@ -1,14 +1,11 @@
from __future__ import print_function
import os
import sys
import py
import pytest
import requests
import time
from deltachat import Account
from deltachat import const
from deltachat.capi import lib
from _pytest.monkeypatch import MonkeyPatch
import tempfile
@@ -18,41 +15,25 @@ def pytest_addoption(parser):
help="a file with >=2 lines where each line "
"contains NAME=VALUE config settings for one account"
)
parser.addoption(
"--ignored", action="store_true",
help="Also run tests marked with the ignored marker",
)
def pytest_configure(config):
config.addinivalue_line(
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
)
cfg = config.getoption('--liveconfig')
if not cfg:
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
cfg = os.getenv('DCC_PY_LIVECONFIG')
if cfg:
config.option.liveconfig = cfg
def pytest_runtest_setup(item):
if (list(item.iter_markers(name="ignored"))
and not item.config.getoption("ignored")):
pytest.skip("Ignored tests not requested, use --ignored")
def pytest_report_header(config, startdir):
summary = []
t = tempfile.mktemp()
m = MonkeyPatch()
try:
m.setattr(sys.stdout, "write", lambda x: len(x))
ac = Account(t)
ac = Account(t, eventlogging=False)
info = ac.get_info()
ac.shutdown()
finally:
m.undo()
os.remove(t)
summary.extend(['Deltachat core={} sqlite={}'.format(
info['deltachat_core_version'],
@@ -102,16 +83,17 @@ class SessionLiveConfigFromFile:
class SessionLiveConfigFromURL:
def __init__(self, url):
def __init__(self, url, create_token):
self.configlist = []
self.url = url
self.create_token = create_token
def get(self, index):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url)
res = requests.post(self.url, json={"token_create_user": int(self.create_token)})
if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
@@ -128,24 +110,14 @@ def session_liveconfig(request):
liveconfig_opt = request.config.option.liveconfig
if liveconfig_opt:
if liveconfig_opt.startswith("http"):
return SessionLiveConfigFromURL(liveconfig_opt)
url, create_token = liveconfig_opt.split("#", 1)
return SessionLiveConfigFromURL(url, create_token)
else:
return SessionLiveConfigFromFile(liveconfig_opt)
@pytest.fixture(scope='session')
def datadir():
"""The py.path.local object of the test-data/ directory."""
for path in reversed(py.path.local(__file__).parts()):
datadir = path.join('test-data')
if datadir.isdir():
return datadir
else:
pytest.skip('test-data directory not found')
@pytest.fixture
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
class AccountMaker:
def __init__(self):
@@ -153,8 +125,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
self.offline_count = 0
self._finalizers = []
self.init_time = time.time()
self._generated_keys = ["alice", "bob", "charlie",
"dom", "elena", "fiona"]
def finalize(self):
while self._finalizers:
@@ -174,32 +144,26 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
ac._evlogger.set_timeout(2)
return ac
def _preconfigure_key(self, account, addr):
# Only set a key if we haven't used it yet for another account.
if self._generated_keys:
keyname = self._generated_keys.pop(0)
fname_pub = "key/{name}-public.asc".format(name=keyname)
fname_sec = "key/{name}-secret.asc".format(name=keyname)
account._preconfigure_keypair(addr,
datadir.join(fname_pub).read(),
datadir.join(fname_sec).read())
def get_configured_offline_account(self):
ac = self.get_unconfigured_account()
# do a pseudo-configured account
addr = "addr{}@offline.org".format(self.offline_count)
ac.set_config("addr", addr)
self._preconfigure_key(ac, addr)
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
ac.set_config("mail_pw", "123")
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
lib.dc_set_config(ac._dc_context, b"configured", b"1")
return ac
def get_online_config(self, pre_generated_key=True):
def peek_online_config(self):
if not session_liveconfig:
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
return session_liveconfig.get(self.live_count)
def get_online_config(self):
if not session_liveconfig:
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
configdict = session_liveconfig.get(self.live_count)
self.live_count += 1
if "e2ee_enabled" not in configdict:
@@ -211,24 +175,18 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, configdict['addr'])
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
return ac, dict(configdict)
def get_online_configuring_account(self, mvbox=False, sentbox=False,
pre_generated_key=True, config={}):
ac, configdict = self.get_online_config(
pre_generated_key=pre_generated_key)
configdict.update(config)
def get_online_configuring_account(self, mvbox=False, sentbox=False):
ac, configdict = self.get_online_config()
ac.configure(**configdict)
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
ac.start_threads()
return ac
def get_one_online_account(self, pre_generated_key=True):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key)
def get_one_online_account(self):
ac1 = self.get_online_configuring_account()
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
return ac1
@@ -242,12 +200,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
wait_configuration_progress(ac2, 1000)
return ac1, ac2
def clone_online_account(self, account, pre_generated_key=True):
def clone_online_account(self, account):
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, account.get_config("addr"))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))

View File

@@ -6,8 +6,7 @@ import time
from deltachat import const, Account
from deltachat.message import Message
from datetime import datetime, timedelta
from conftest import (wait_configuration_progress,
wait_securejoin_inviter_progress)
from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection, wait_securejoin_inviter_progress
class TestOfflineAccountBasic:
@@ -25,12 +24,6 @@ class TestOfflineAccountBasic:
ac1 = Account(p.strpath, os_name="solarpunk")
ac1.get_info()
def test_preconfigure_keypair(self, acfactory, datadir):
ac = acfactory.get_unconfigured_account()
ac._preconfigure_keypair("alice@example.com",
datadir.join("key/alice-public.asc").read(),
datadir.join("key/alice-secret.asc").read())
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
d = ac1.get_info()
@@ -65,6 +58,11 @@ class TestOfflineAccountBasic:
with pytest.raises(ValueError):
ac1.get_self_contact()
def test_get_info(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
out = ac1.get_infostring()
assert "number_of_chats=0" in out
def test_selfcontact_configured(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
me = ac1.get_self_contact()
@@ -178,6 +176,7 @@ class TestOfflineChat:
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1):
ac1.start_threads()
ac1.set_stock_translation(const.DC_STR_NEWGROUPDRAFT, "xyz %1$s")
ac1._evlogger.consume_events()
with pytest.raises(ValueError):
@@ -197,6 +196,7 @@ class TestOfflineChat:
assert not chat.is_promoted()
msg = chat.get_draft()
assert msg.text == "xyz title1"
ac1.stop_threads()
@pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified):
@@ -214,18 +214,6 @@ class TestOfflineChat:
chat.remove_profile_image()
assert chat.get_profile_image() is None
def test_mute(self, ac1):
chat = ac1.create_group_chat(name="title1")
assert not chat.is_muted()
chat.mute()
assert chat.is_muted()
chat.unmute()
assert not chat.is_muted()
chat.mute(50)
assert chat.is_muted()
with pytest.raises(ValueError):
chat.mute(-51)
def test_delete_and_send_fails(self, ac1, chat1):
chat1.delete()
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
@@ -431,45 +419,6 @@ class TestOnlineAccount:
ac2.create_chat_by_contact(ac2.create_contact(email=ac1.get_config("addr")))
return chat
@pytest.mark.ignored
def test_configure_generate_key(self, acfactory, lp):
# A slow test which will generate new keys.
ac1 = acfactory.get_online_configuring_account(
pre_generated_key=False,
config={"key_gen_type": str(const.DC_KEY_GEN_RSA2048)}
)
ac2 = acfactory.get_online_configuring_account(
pre_generated_key=False,
config={"key_gen_type": str(const.DC_KEY_GEN_ED25519)}
)
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
chat = self.get_chat(ac1, ac2, both_created=True)
lp.sec("ac1: send unencrypted message to ac2")
chat.send_text("message1")
lp.sec("ac2: waiting for message from ac1")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
msg_in = ac2.get_message_by_id(ev[2])
assert msg_in.text == "message1"
assert not msg_in.is_encrypted()
lp.sec("ac2: send encrypted message to ac1")
msg_in.chat.send_text("message2")
lp.sec("ac1: waiting for message from ac2")
ev = ac1._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
msg2_in = ac1.get_message_by_id(ev[2])
assert msg2_in.text == "message2"
assert msg2_in.is_encrypted()
lp.sec("ac1: send encrypted message to ac2")
msg2_in.chat.send_text("message3")
lp.sec("ac2: waiting for message from ac1")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
msg3_in = ac1.get_message_by_id(ev[2])
assert msg3_in.text == "message3"
assert msg3_in.is_encrypted()
def test_configure_canceled(self, acfactory):
ac1 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 200)
@@ -488,53 +437,37 @@ class TestOnlineAccount:
def test_one_account_send_bcc_setting(self, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
# Clone the first account: we will test if sent messages
# are copied to it via BCC.
ac1_clone = acfactory.clone_online_account(ac1)
ac2_config = acfactory.peek_online_config()
c2 = ac1.create_contact(email=ac2_config["addr"])
chat = ac1.create_chat_by_contact(c2)
assert chat.id > const.DC_CHAT_ID_LAST_SPECIAL
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
wait_configuration_progress(ac1_clone, 1000)
chat = self.get_chat(ac1, ac2)
self_addr = ac1.get_config("addr")
other_addr = ac2.get_config("addr")
lp.sec("send out message without bcc to ourselves")
ac1.set_config("bcc_self", "0")
msg_out = chat.send_text("message1")
assert not msg_out.is_forwarded()
# wait for send out (no BCC)
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "0"
# make sure we are not sending message to ourselves
assert self_addr not in ev[2]
assert other_addr in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
lp.sec("ac1: setting bcc_self=1")
ac1.set_config("bcc_self", "1")
lp.sec("send out message with bcc to ourselves")
msg_out = chat.send_text("message2")
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev[2] == msg_out.id
# wait for send out (BCC)
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "1"
# now make sure we are sending message to ourselves too
self_addr = ac1.get_config("addr")
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert self_addr in ev[2]
assert other_addr in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
# Second client receives only second message, but not the first
ev = ac1_clone._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ac1_clone.get_message_by_id(ev[2]).text == msg_out.text
ac1._evlogger.consume_events()
lp.sec("send out message without bcc")
ac1.set_config("bcc_self", "0")
msg_out = chat.send_text("message3")
assert not msg_out.is_forwarded()
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev[2] == msg_out.id
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert self_addr not in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
def test_send_file_twice_unicode_filename_mangling(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -731,7 +664,7 @@ class TestOnlineAccount:
assert not msg_in.is_forwarded()
assert msg_in.get_sender_contact().display_name == ac1.get_config("displayname")
lp.sec("check the message arrived in contact-requests/deaddrop")
lp.sec("check the message arrived in contact-requets/deaddrop")
chat2 = msg_in.chat
assert msg_in in chat2.get_messages()
assert chat2.is_deaddrop()

View File

@@ -1,29 +1,64 @@
from __future__ import print_function
import threading
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
from deltachat.capi import ffi
from deltachat.capi import lib
from deltachat.account import EventLogger
class EventThread(threading.Thread):
def __init__(self, dc_context):
self.dc_context = dc_context
super(EventThread, self).__init__()
self.setDaemon(1)
def run(self):
lib.dc_context_run(self.dc_context, lib.py_dc_callback)
def stop(self):
lib.dc_context_shutdown(self.dc_context)
def test_empty_context():
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL)
capi.lib.dc_close(ctx)
def test_callback_None2int():
ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL)
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
set_context_callback(ctx, lambda *args: None)
capi.lib.dc_close(ctx)
clear_context_callback(ctx)
def test_start_stop_event_thread_basic():
print("1")
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
print("2")
ev_thread = EventThread(ctx)
print("3 -- starting event thread")
ev_thread.start()
print("4 -- stopping event thread")
ev_thread.stop()
def test_dc_close_events(tmpdir):
from deltachat.account import Account
ctx = ffi.gc(
capi.lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
evlog = EventLogger(ctx)
evlog.set_timeout(5)
set_context_callback(
ctx,
lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2)
)
ev_thread = EventThread(ctx)
ev_thread.start()
p = tmpdir.join("hello.db")
ac1 = Account(p.strpath)
ac1.shutdown()
lib.dc_open(ctx, p.strpath.encode("ascii"), ffi.NULL)
capi.lib.dc_close(ctx)
def find(info_string):
evlog = ac1._evlogger
while 1:
ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
data2 = ev[2]
@@ -37,11 +72,12 @@ def test_dc_close_events(tmpdir):
find("disconnecting mvbox-thread")
find("disconnecting SMTP")
find("Database closed")
ev_thread.stop()
def test_wrong_db(tmpdir):
dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
p = tmpdir.join("hello.db")
@@ -53,7 +89,7 @@ def test_wrong_db(tmpdir):
def test_empty_blobdir(tmpdir):
# Apparently some client code expects this to be the same as passing NULL.
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("hello.db")
@@ -93,17 +129,24 @@ def test_get_special_message_id_returns_empty_message(acfactory):
assert msg.id == 0
def test_provider_info():
provider = lib.dc_provider_new_from_email(cutil.as_dc_charpointer("ex@example.com"))
assert cutil.from_dc_charpointer(
lib.dc_provider_get_overview_page(provider)
) == "https://providers.delta.chat/example.com"
assert cutil.from_dc_charpointer(lib.dc_provider_get_name(provider)) == "Example"
assert cutil.from_dc_charpointer(lib.dc_provider_get_markdown(provider)) == "\n..."
assert cutil.from_dc_charpointer(lib.dc_provider_get_status_date(provider)) == "2018-09"
assert lib.dc_provider_get_status(provider) == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_none():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
assert lib.dc_provider_new_from_email(cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
def test_get_info_closed():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
@@ -113,7 +156,7 @@ def test_get_info_closed():
def test_get_info_open(tmpdir):
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("test.db")
@@ -125,7 +168,7 @@ def test_get_info_open(tmpdir):
def test_is_open_closed():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_is_open(ctx) == 0
@@ -133,7 +176,7 @@ def test_is_open_closed():
def test_is_open_actually_open(tmpdir):
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("test.db")

View File

@@ -0,0 +1,27 @@
import pytest
from deltachat import const
from deltachat import provider
def test_provider_info_from_email():
example = provider.Provider.from_email("email@example.com")
assert example.overview_page == "https://providers.delta.chat/example.com"
assert example.name == "Example"
assert example.markdown == "\n..."
assert example.status_date == "2018-09"
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_from_domain():
example = provider.Provider("example.com")
assert example.overview_page == "https://providers.delta.chat/example.com"
assert example.name == "Example"
assert example.markdown == "\n..."
assert example.status_date == "2018-09"
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_none():
with pytest.raises(provider.ProviderNotFoundError):
provider.Provider.from_email("email@unexistent.no")

View File

@@ -7,7 +7,7 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs:tests}
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx {posargs:tests}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
@@ -65,7 +65,7 @@ commands =
[pytest]
addopts = -v -ra --strict-markers
addopts = -v -ra
python_files = tests/test_*.py
norecursedirs = .tox
xfail_strict=true

View File

@@ -1 +1 @@
nightly-2019-11-06
nightly-2020-01-26

View File

@@ -1,80 +0,0 @@
#!/usr/bin/env python3
# Examples:
#
# Original server that doesn't use SSL:
# ./proxy.py 8080 imap.nauta.cu 143
# ./proxy.py 8081 smtp.nauta.cu 25
#
# Original server that uses SSL:
# ./proxy.py 8080 testrun.org 993 --ssl
# ./proxy.py 8081 testrun.org 465 --ssl
from datetime import datetime
import argparse
import selectors
import ssl
import socket
import socketserver
class Proxy(socketserver.ThreadingTCPServer):
allow_reuse_address = True
def __init__(self, proxy_host, proxy_port, real_host, real_port, use_ssl):
self.real_host = real_host
self.real_port = real_port
self.use_ssl = use_ssl
super().__init__((proxy_host, proxy_port), RequestHandler)
class RequestHandler(socketserver.BaseRequestHandler):
def handle(self):
print('{} - {} CONNECTED.'.format(datetime.now(), self.client_address))
total = 0
real_server = (self.server.real_host, self.server.real_port)
with socket.create_connection(real_server) as sock:
if self.server.use_ssl:
context = ssl.create_default_context()
sock = context.wrap_socket(
sock, server_hostname=real_server[0])
forward = {self.request: sock, sock: self.request}
sel = selectors.DefaultSelector()
sel.register(self.request, selectors.EVENT_READ,
self.client_address)
sel.register(sock, selectors.EVENT_READ, real_server)
active = True
while active:
events = sel.select()
for key, mask in events:
print('\n{} - {} wrote:'.format(datetime.now(), key.data))
data = key.fileobj.recv(1024)
received = len(data)
total += received
print(data)
print('{} Bytes\nTotal: {} Bytes'.format(received, total))
if data:
forward[key.fileobj].sendall(data)
else:
print('\nCLOSING CONNECTION.\n\n')
forward[key.fileobj].close()
key.fileobj.close()
active = False
if __name__ == '__main__':
p = argparse.ArgumentParser(description='Simple Python Proxy')
p.add_argument(
"proxy_port", help="the port where the proxy will listen", type=int)
p.add_argument('host', help="the real host")
p.add_argument('port', help="the port of the real host", type=int)
p.add_argument("--ssl", help="use ssl to connect to the real host",
action="store_true")
args = p.parse_args()
with Proxy('', args.proxy_port, args.host, args.port, args.ssl) as proxy:
proxy.serve_forever()

View File

@@ -35,7 +35,7 @@ if __name__ == "__main__":
if len(sys.argv) < 2:
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
print("{}: {}".format(x, read_toml_version(x)))
raise SystemExit("need argument: new version, example: 1.25.0")
raise SystemExit("need argument: new version, example 1.0.0-beta.27")
newversion = sys.argv[1]
if newversion.count(".") < 2:
raise SystemExit("need at least two dots in version")
@@ -45,7 +45,7 @@ if __name__ == "__main__":
assert core_toml == ffi_toml, (core_toml, ffi_toml)
for line in open("CHANGELOG.md"):
## 1.25.0
## 1.0.0-beta5
if line.startswith("## "):
if line[2:].strip().startswith(newversion):
break

94
spec.md
View File

@@ -1,6 +1,6 @@
# Chat-over-Email specification
Version 0.30.0
Version 0.20.0
This document describes how emails can be used
to implement typical messenger functions
@@ -17,9 +17,6 @@ while staying compatible to existing MUAs.
- [Change group name](#change-group-name)
- [Set group image](#set-group-image)
- [Set profile image](#set-profile-image)
- [Locations](#locations)
- [User locations](#user-locations)
- [Points of interest](#points-of-interest)
- [Miscellaneous](#miscellaneous)
@@ -32,7 +29,8 @@ Messages SHOULD be encrypted by the
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
by the [Memoryhole](https://github.com/autocrypt/memoryhole) standard.
If Memoryhole is not used,
the subject of encrypted messages SHOULD be replaced by the string `...`.
the subject of encrypted messages SHOULD be replaced by the string
`Chat: Encrypted message` where the part after the colon MAY be localized.
# Outgoing messages
@@ -115,7 +113,6 @@ but MUAs typically expose the sender in the UI.
Groups are chats with usually more than one recipient,
each defined by an email-address.
The sender plus the recipients are the group members.
All group members form the member list.
To allow different groups with the same members,
groups are identified by a group-id.
@@ -138,7 +135,8 @@ The group-name MUST be written to `Chat-Group-Name` header
to join a group chat on a second device any time).
The `Subject` header of outgoing group messages
SHOULD be set to the group-name.
SHOULD start with the characters `Chat:`
followed by the group-name and a colon followed by an excerpt of the message.
To identify the group-id on replies from normal MUAs,
the group-id MUST also be added to the message-id of outgoing messages.
@@ -179,22 +177,12 @@ to a normal single-user chat with the email-address given in `From`.
## Add and remove members
Messenger clients MUST init the member list
from the `From`/`To` headers on the first group message.
When a member is added later,
a `Chat-Group-Member-Added` action header must be sent
with the value set to the email-address of the added member.
When receiving a `Chat-Group-Member-Added` header, however,
_all missing_ members the `From`/`To` headers has to be added.
This is to mitigate problems when receiving messages
in different orders, esp. on creating new groups.
To remove a member, a `Chat-Group-Member-Removed` header must be sent
with the value set to the email-address of the member to remove.
When receiving a `Chat-Group-Member-Removed` header,
only exaxtly the given member has to be removed from the member list.
Messenger clients MUST construct the member list
from the `From`/`To` headers only on the first group message
or if they see a `Chat-Group-Member-Added`
or `Chat-Group-Member-Removed` action header.
Both headers MUST have the email-address
of the added or removed member as the value.
Messenger clients MUST NOT construct the member list
on other group messages
(this is to avoid accidentally altered To-lists in normal MUAs;
@@ -344,64 +332,6 @@ To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
only on image changes.
# Locations
Locations can be attachted to messages using
[standard kml-files](https://www.opengeospatial.org/standards/kml/)
with well-known names.
## User locations
To send the location of the sender,
the app can attach a file with the name `location.kml`.
The file can contain one or more locations.
Apps that support location streaming will typically collect some location events
and send them together in one file.
As each location has an independent timestamp,
the apps can show the location as a track.
Note that the `addr` attribute inside the `location.kml` file
MUST match the users email-address.
Otherwise, the file is discarded silently;
this is to protect against getting wrong locations,
eg. forwarded from a normal MUA.
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document addr="ndh@deltachat.de">
<Placemark>
<Timestamp><when>2020-01-11T20:40:19Z</when></Timestamp>
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
</Placemark>
<Placemark>
<Timestamp><when>2020-01-11T20:40:25Z</when></Timestamp>
<Point><coordinates accuracy="5.4">7.654,3.21</coordinates></Point>
</Placemark>
</Document>
</kml>
## Points of interest
To send an "Point of interest", a POI,
use a normal message and attach a file with the name `message.kml`.
In contrast to user locations, this file should contain only one location
and an address-attribute is not needed -
as the location belongs to the message content,
it is fine if the location is detected on forwarding etc.
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<Timestamp><when>2020-01-01T20:40:19Z</when></Timestamp>
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
</Placemark>
</Document>
</kml>
# Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual.
@@ -438,4 +368,4 @@ as the sending time of the message as indicated by its Date header,
or the time of first receipt if that date is in the future or unavailable.
Copyright © 2017-2020 Delta Chat contributors.
Copyright © 2017-2019 Delta Chat contributors.

View File

@@ -6,10 +6,10 @@ use std::collections::BTreeMap;
use std::str::FromStr;
use std::{fmt, str};
use crate::constants::*;
use crate::contact::*;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, SignedPublicKey};
use crate::key::*;
/// Possible values for encryption preference
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
@@ -52,17 +52,13 @@ impl str::FromStr for EncryptPreference {
#[derive(Debug)]
pub struct Aheader {
pub addr: String,
pub public_key: SignedPublicKey,
pub public_key: Key,
pub prefer_encrypt: EncryptPreference,
}
impl Aheader {
/// Creates new autocrypt header
pub fn new(
addr: String,
public_key: SignedPublicKey,
prefer_encrypt: EncryptPreference,
) -> Self {
pub fn new(addr: String, public_key: Key, prefer_encrypt: EncryptPreference) -> Self {
Aheader {
addr,
public_key,
@@ -75,7 +71,9 @@ impl Aheader {
wanted_from: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Option<Self> {
if let Ok(Some(value)) = headers.get_header_value(HeaderDef::Autocrypt) {
use mailparse::MailHeaderMap;
if let Ok(Some(value)) = headers.get_first_value("Autocrypt") {
match Self::from_str(&value) {
Ok(header) => {
if addr_cmp(&header.addr, wanted_from) {
@@ -144,11 +142,22 @@ impl str::FromStr for Aheader {
return Err(());
}
};
let public_key: SignedPublicKey = attributes
let public_key = match attributes
.remove("keydata")
.ok_or(())
.and_then(|raw| SignedPublicKey::from_base64(&raw).or(Err(())))
.and_then(|key| key.verify().and(Ok(key)).or(Err(())))?;
.and_then(|raw| Key::from_base64(&raw, KeyType::Public))
{
Some(key) => {
if key.verify() {
key
} else {
return Err(());
}
}
None => {
return Err(());
}
};
let prefer_encrypt = attributes
.remove("prefer-encrypt")
@@ -283,7 +292,7 @@ mod tests {
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
EncryptPreference::Mutual
)
)
@@ -296,7 +305,7 @@ mod tests {
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
EncryptPreference::NoPreference
)
)

View File

@@ -1,8 +1,6 @@
//! # Chat module
use std::convert::TryFrom;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use itertools::Itertools;
use num_traits::FromPrimitive;
@@ -28,9 +26,7 @@ use crate::stock::StockMessage;
///
/// Some chat IDs are reserved to identify special chat types. This
/// type can represent both the special as well as normal chats.
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord,
)]
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatId(u32);
impl ChatId {
@@ -49,7 +45,7 @@ impl ChatId {
/// An unset ChatId
///
/// Like [ChatId::is_error], from which it is indistinguishable, this is
/// Like [is_error], from which it is indistinguishable, this is
/// transitional and should not be used in new code.
pub fn is_unset(self) -> bool {
self.0 == 0
@@ -137,18 +133,14 @@ impl ChatId {
}
/// Archives or unarchives a chat.
pub fn set_visibility(
self,
context: &Context,
visibility: ChatVisibility,
) -> Result<(), Error> {
pub fn set_archived(self, context: &Context, new_archived: bool) -> Result<(), Error> {
ensure!(
!self.is_special(),
"bad chat_id, can not be special chat: {}",
self
);
if visibility == ChatVisibility::Archived {
if new_archived {
sql::execute(
context,
&context.sql,
@@ -161,7 +153,7 @@ impl ChatId {
context,
&context.sql,
"UPDATE chats SET archived=? WHERE id=?;",
params![visibility, self],
params![new_archived, self],
)?;
context.call_cb(Event::MsgsChanged {
@@ -172,13 +164,13 @@ impl ChatId {
Ok(())
}
// note that unarchive() is not the same as set_visibility(Normal) -
// eg. unarchive() does not modify pinned chats and does not send events.
// note that unarchive() is not the same as set_archived(false) -
// eg. unarchive() does not send events as done for set_archived(false).
pub fn unarchive(self, context: &Context) -> Result<(), Error> {
sql::execute(
context,
&context.sql,
"UPDATE chats SET archived=0 WHERE id=? and archived=1",
"UPDATE chats SET archived=0 WHERE id=?",
params![self],
)?;
Ok(())
@@ -420,17 +412,16 @@ impl rusqlite::types::FromSql for ChatId {
/// Chat objects are created using eg. `Chat::load_from_db`
/// and are not updated on database changes;
/// if you want an update, you have to recreate the object.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Debug, Clone)]
pub struct Chat {
pub id: ChatId,
pub typ: Chattype,
pub name: String,
pub visibility: ChatVisibility,
archived: bool,
pub grpid: String,
blocked: Blocked,
pub param: Params,
is_sending_locations: bool,
pub mute_duration: MuteDuration,
}
impl Chat {
@@ -438,7 +429,7 @@ impl Chat {
pub fn load_from_db(context: &Context, chat_id: ChatId) -> Result<Self, Error> {
let res = context.sql.query_row(
"SELECT c.type, c.name, c.grpid, c.param, c.archived,
c.blocked, c.locations_send_until, c.muted_until
c.blocked, c.locations_send_until
FROM chats c
WHERE c.id=?;",
params![chat_id],
@@ -449,10 +440,9 @@ impl Chat {
name: row.get::<_, String>(1)?,
grpid: row.get::<_, String>(2)?,
param: row.get::<_, String>(3)?.parse().unwrap_or_default(),
visibility: row.get(4)?,
archived: row.get(4)?,
blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(),
is_sending_locations: row.get(6)?,
mute_duration: row.get(7)?,
};
Ok(c)
},
@@ -593,19 +583,23 @@ impl Chat {
sql.query_row(&query, params, collect).ok()
}
fn parent_is_encrypted(&self, context: &Context) -> Result<bool, Error> {
fn parent_is_encrypted(&self, context: &Context) -> bool {
let sql = &context.sql;
let params = params![self.id];
let query = Self::parent_query("param");
let packed: Option<String> = sql.query_get_value_result(&query, params)?;
let packed: Option<String> = sql.query_get_value(context, &query, params);
if let Some(ref packed) = packed {
let param = packed.parse::<Params>()?;
Ok(param.exists(Param::GuaranteeE2ee))
match packed.parse::<Params>() {
Ok(param) => param.exists(Param::GuaranteeE2ee),
Err(err) => {
error!(context, "invalid params stored: '{}', {:?}", packed, err);
false
}
}
} else {
// No messages
Ok(false)
false
}
}
@@ -660,7 +654,7 @@ impl Chat {
id: self.id,
type_: self.typ as u32,
name: self.name.clone(),
archived: self.visibility == ChatVisibility::Archived,
archived: self.archived,
param: self.param.to_string(),
gossiped_timestamp: self.get_gossiped_timestamp(context),
is_sending_locations: self.is_sending_locations,
@@ -668,12 +662,12 @@ impl Chat {
profile_image: self.get_profile_image(context).unwrap_or_else(PathBuf::new),
subtitle: self.get_subtitle(context),
draft,
is_muted: self.is_muted(),
})
}
pub fn get_visibility(&self) -> ChatVisibility {
self.visibility
/// Returns true if the chat is archived.
pub fn is_archived(&self) -> bool {
self.archived
}
pub fn is_unpromoted(&self) -> bool {
@@ -694,14 +688,6 @@ impl Chat {
self.is_sending_locations
}
pub fn is_muted(&self) -> bool {
match self.mute_duration {
MuteDuration::NotMuted => false,
MuteDuration::Forever => true,
MuteDuration::Until(when) => when > SystemTime::now(),
}
}
fn prepare_msg_raw(
&mut self,
context: &Context,
@@ -815,7 +801,7 @@ impl Chat {
}
}
if can_encrypt && (all_mutual || self.parent_is_encrypted(context)?) {
if can_encrypt && (all_mutual || self.parent_is_encrypted(context)) {
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
}
@@ -931,40 +917,6 @@ impl Chat {
}
}
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub enum ChatVisibility {
Normal,
Archived,
Pinned,
}
impl rusqlite::types::ToSql for ChatVisibility {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let visibility = match &self {
ChatVisibility::Normal => 0,
ChatVisibility::Archived => 1,
ChatVisibility::Pinned => 2,
};
let val = rusqlite::types::Value::Integer(visibility);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
impl rusqlite::types::FromSql for ChatVisibility {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|val| {
match val {
2 => Ok(ChatVisibility::Pinned),
1 => Ok(ChatVisibility::Archived),
0 => Ok(ChatVisibility::Normal),
// fallback to to Normal for unknown values, may happen eg. on imports created by a newer version.
_ => Ok(ChatVisibility::Normal),
}
})
}
}
/// The current state of a chat.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
@@ -1020,11 +972,6 @@ pub struct ChatInfo {
/// which contain non-text parts. Perhaps it should be a
/// simple `has_draft` bool instead.
pub draft: String,
/// Whether the chat is muted
///
/// The exact time its muted can be found out via the `chat.mute_duration` property
pub is_muted: bool,
// ToDo:
// - [ ] deaddrop,
// - [ ] summary,
@@ -1114,7 +1061,7 @@ pub fn create_by_contact_id(context: &Context, contact_id: u32) -> Result<ChatId
Ok(chat_id)
}
pub(crate) fn update_saved_messages_icon(context: &Context) -> Result<(), Error> {
pub fn update_saved_messages_icon(context: &Context) -> Result<(), Error> {
// if there is no saved-messages chat, there is nothing to update. this is no error.
if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_SELF) {
let icon = include_bytes!("../assets/icon-saved-messages.png");
@@ -1128,7 +1075,7 @@ pub(crate) fn update_saved_messages_icon(context: &Context) -> Result<(), Error>
Ok(())
}
pub(crate) fn update_device_icon(context: &Context) -> Result<(), Error> {
pub fn update_device_icon(context: &Context) -> Result<(), Error> {
// if there is no device-chat, there is nothing to update. this is no error.
if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE) {
let icon = include_bytes!("../assets/icon-device.png");
@@ -1162,13 +1109,13 @@ fn update_special_chat_name(
Ok(())
}
pub(crate) fn update_special_chat_names(context: &Context) -> Result<(), Error> {
pub fn update_special_chat_names(context: &Context) -> Result<(), Error> {
update_special_chat_name(context, DC_CONTACT_ID_DEVICE, StockMessage::DeviceMessages)?;
update_special_chat_name(context, DC_CONTACT_ID_SELF, StockMessage::SavedMessages)?;
Ok(())
}
pub(crate) fn create_or_lookup_by_contact_id(
pub fn create_or_lookup_by_contact_id(
context: &Context,
contact_id: u32,
create_blocked: Blocked,
@@ -1184,31 +1131,33 @@ pub(crate) fn create_or_lookup_by_contact_id(
let contact = Contact::load_from_db(context, contact_id)?;
let chat_name = contact.get_display_name();
context
.sql
.start_stmt("create_or_lookup_by_contact_id transaction");
context.sql.with_conn(|conn| {
let tx = conn.transaction()?;
tx.execute(
"INSERT INTO chats (type, name, param, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?)",
sql::execute(
context,
&context.sql,
"INSERT INTO chats (type, name, param, blocked, grpid, created_timestamp) VALUES(?, ?, ?, ?, ?, ?)",
params![
Chattype::Single,
100,
chat_name,
match contact_id {
DC_CONTACT_ID_SELF => "K=1".to_string(), // K = Param::Selftalk
DC_CONTACT_ID_DEVICE => "D=1".to_string(), // D = Param::Devicetalk
_ => "".to_string(),
_ => "".to_string()
},
create_blocked as u8,
contact.get_addr(),
time(),
]
)?;
tx.execute(
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES((SELECT last_insert_rowid()), ?)",
params![contact_id])?;
tx.commit()?;
Ok(())
})?;
)?;
let row_id = sql::get_rowid(context, &context.sql, "chats", "grpid", contact.get_addr());
let chat_id = ChatId::new(row_id);
sql::execute(
context,
&context.sql,
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
params![chat_id, contact_id],
)?;
if contact_id == DC_CONTACT_ID_SELF {
update_saved_messages_icon(context)?;
@@ -1216,10 +1165,10 @@ pub(crate) fn create_or_lookup_by_contact_id(
update_device_icon(context)?;
}
lookup_by_contact_id(context, contact_id)
Ok((chat_id, create_blocked))
}
pub(crate) fn lookup_by_contact_id(
pub fn lookup_by_contact_id(
context: &Context,
contact_id: u32,
) -> Result<(ChatId, Blocked), Error> {
@@ -1273,7 +1222,7 @@ pub fn prepare_msg<'a>(
Ok(msg_id)
}
pub(crate) fn msgtype_has_file(msgtype: Viewtype) -> bool {
pub fn msgtype_has_file(msgtype: Viewtype) -> bool {
match msgtype {
Viewtype::Unknown => false,
Viewtype::Text => false,
@@ -1750,11 +1699,9 @@ pub fn create_group_chat(
Ok(chat_id)
}
pub(crate) fn add_to_chat_contacts_table(
context: &Context,
chat_id: ChatId,
contact_id: u32,
) -> bool {
/* you MUST NOT modify this or the following strings */
// Context functions to work with chats
pub fn add_to_chat_contacts_table(context: &Context, chat_id: ChatId, contact_id: u32) -> bool {
// add a contact to a chat; the function does not check the type or if any of the record exist or are already
// added to the chat!
sql::execute(
@@ -1766,22 +1713,6 @@ pub(crate) fn add_to_chat_contacts_table(
.is_ok()
}
pub(crate) fn remove_from_chat_contacts_table(
context: &Context,
chat_id: ChatId,
contact_id: u32,
) -> bool {
// remove a contact from the chats_contact table unconditionally
// the function does not check the type or if the record exist
sql::execute(
context,
&context.sql,
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
params![chat_id, contact_id as i32],
)
.is_ok()
}
/// Adds a contact to the chat.
pub fn add_contact_to_chat(context: &Context, chat_id: ChatId, contact_id: u32) -> bool {
match add_contact_to_chat_ex(context, chat_id, contact_id, false) {
@@ -1902,7 +1833,7 @@ fn real_group_exists(context: &Context, chat_id: ChatId) -> bool {
.unwrap_or_default()
}
pub(crate) fn reset_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<(), Error> {
pub fn reset_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result<(), Error> {
set_gossiped_timestamp(context, chat_id, 0)
}
@@ -1919,7 +1850,7 @@ pub fn get_gossiped_timestamp(context: &Context, chat_id: ChatId) -> i64 {
.unwrap_or_default()
}
pub(crate) fn set_gossiped_timestamp(
pub fn set_gossiped_timestamp(
context: &Context,
chat_id: ChatId,
timestamp: i64,
@@ -1939,7 +1870,7 @@ pub(crate) fn set_gossiped_timestamp(
Ok(())
}
pub(crate) fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool, Error> {
pub fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool, Error> {
// versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others.
// to avoid sending out previously set selfavatars unexpectedly we added this additional check.
// it can be removed after some time.
@@ -1974,66 +1905,6 @@ pub(crate) fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Res
Ok(needs_attach)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum MuteDuration {
NotMuted,
Forever,
Until(SystemTime),
}
impl rusqlite::types::ToSql for MuteDuration {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let duration: i64 = match &self {
MuteDuration::NotMuted => 0,
MuteDuration::Forever => -1,
MuteDuration::Until(when) => {
let duration = when
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
i64::try_from(duration.as_secs())
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?
}
};
let val = rusqlite::types::Value::Integer(duration);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
impl rusqlite::types::FromSql for MuteDuration {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
// Negative values other than -1 should not be in the
// database. If found they'll be NotMuted.
match i64::column_result(value)? {
0 => Ok(MuteDuration::NotMuted),
-1 => Ok(MuteDuration::Forever),
n if n > 0 => match SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(n as u64)) {
Some(t) => Ok(MuteDuration::Until(t)),
None => Err(rusqlite::types::FromSqlError::OutOfRange(n)),
},
_ => Ok(MuteDuration::NotMuted),
}
}
}
pub fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuration) -> Result<(), Error> {
ensure!(!chat_id.is_special(), "Invalid chat ID");
if real_group_exists(context, chat_id)
&& sql::execute(
context,
&context.sql,
"UPDATE chats SET muted_until=? WHERE id=?;",
params![duration, chat_id],
)
.is_ok()
{
context.call_cb(Event::ChatModified(chat_id));
} else {
bail!("Failed to set name");
}
Ok(())
}
pub fn remove_contact_from_chat(
context: &Context,
chat_id: ChatId,
@@ -2093,7 +1964,14 @@ pub fn remove_contact_from_chat(
});
}
}
if remove_from_chat_contacts_table(context, chat_id, contact_id) {
if sql::execute(
context,
&context.sql,
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
params![chat_id, contact_id as i32],
)
.is_ok()
{
context.call_cb(Event::ChatModified(chat_id));
success = true;
}
@@ -2121,10 +1999,7 @@ fn set_group_explicitly_left(context: &Context, grpid: impl AsRef<str>) -> Resul
Ok(())
}
pub(crate) fn is_group_explicitly_left(
context: &Context,
grpid: impl AsRef<str>,
) -> Result<bool, Error> {
pub fn is_group_explicitly_left(context: &Context, grpid: impl AsRef<str>) -> Result<bool, Error> {
context
.sql
.exists(
@@ -2346,7 +2221,7 @@ pub fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Re
Ok(())
}
pub(crate) fn get_chat_contact_cnt(context: &Context, chat_id: ChatId) -> usize {
pub fn get_chat_contact_cnt(context: &Context, chat_id: ChatId) -> usize {
context
.sql
.query_get_value::<_, isize>(
@@ -2357,7 +2232,7 @@ pub(crate) fn get_chat_contact_cnt(context: &Context, chat_id: ChatId) -> usize
.unwrap_or_default() as usize
}
pub(crate) fn get_chat_cnt(context: &Context) -> usize {
pub fn get_chat_cnt(context: &Context) -> usize {
if context.sql.is_open() {
/* no database, no chats - this is no error (needed eg. for information) */
context
@@ -2472,7 +2347,7 @@ pub fn was_device_msg_ever_added(context: &Context, label: &str) -> Result<bool,
// no wrong information are shown in the device chat
// - deletion in `devmsglabels` makes sure,
// deleted messages are resetted and useful messages can be added again
pub(crate) fn delete_and_reset_all_device_msgs(context: &Context) -> Result<(), Error> {
pub fn delete_and_reset_all_device_msgs(context: &Context) -> Result<(), Error> {
context.sql.execute(
"DELETE FROM msgs WHERE from_id=?;",
params![DC_CONTACT_ID_DEVICE],
@@ -2486,7 +2361,7 @@ pub(crate) fn delete_and_reset_all_device_msgs(context: &Context) -> Result<(),
/// Adds an informational message to chat.
///
/// For example, it can be a message showing that a member was added to a group.
pub(crate) fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef<str>) {
pub fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef<str>) {
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
if context.sql.execute(
@@ -2527,7 +2402,7 @@ mod tests {
let chat = Chat::load_from_db(&t.ctx, chat_id).unwrap();
let info = chat.get_info(&t.ctx).unwrap();
// Ensure we can serialize this.
// Ensure we can serialise this.
println!("{}", serde_json::to_string_pretty(&info).unwrap());
let expected = r#"
@@ -2542,12 +2417,11 @@ mod tests {
"color": 15895624,
"profile_image": "",
"subtitle": "bob@example.com",
"draft": "",
"is_muted": false
"draft": ""
}
"#;
// Ensure we can deserialize this.
// Ensure we can deserialise this.
let loaded: ChatInfo = serde_json::from_str(expected).unwrap();
assert_eq!(info, loaded);
}
@@ -2609,7 +2483,7 @@ mod tests {
let chat = Chat::load_from_db(&t.ctx, chat_id).unwrap();
assert_eq!(chat.id, chat_id);
assert!(chat.is_self_talk());
assert!(chat.visibility == ChatVisibility::Normal);
assert!(!chat.archived);
assert!(!chat.is_device_talk());
assert!(chat.can_send());
assert_eq!(chat.name, t.ctx.stock_str(StockMessage::SavedMessages));
@@ -2623,7 +2497,7 @@ mod tests {
assert_eq!(DC_CHAT_ID_DEADDROP, 1);
assert!(chat.id.is_deaddrop());
assert!(!chat.is_self_talk());
assert!(chat.visibility == ChatVisibility::Normal);
assert!(!chat.archived);
assert!(!chat.is_device_talk());
assert!(!chat.can_send());
assert_eq!(chat.name, t.ctx.stock_str(StockMessage::DeadDrop));
@@ -2829,134 +2703,35 @@ mod tests {
assert_eq!(DC_GCL_NO_SPECIALS, 0x02);
// archive first chat
assert!(chat_id1
.set_visibility(&t.ctx, ChatVisibility::Archived)
.is_ok());
assert!(
Chat::load_from_db(&t.ctx, chat_id1)
.unwrap()
.get_visibility()
== ChatVisibility::Archived
);
assert!(
Chat::load_from_db(&t.ctx, chat_id2)
.unwrap()
.get_visibility()
== ChatVisibility::Normal
);
assert!(chat_id1.set_archived(&t.ctx, true).is_ok());
assert!(Chat::load_from_db(&t.ctx, chat_id1).unwrap().is_archived());
assert!(!Chat::load_from_db(&t.ctx, chat_id2).unwrap().is_archived());
assert_eq!(get_chat_cnt(&t.ctx), 2);
assert_eq!(chatlist_len(&t.ctx, 0), 2); // including DC_CHAT_ID_ARCHIVED_LINK now
assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS), 1);
assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 1);
// archive second chat
assert!(chat_id2
.set_visibility(&t.ctx, ChatVisibility::Archived)
.is_ok());
assert!(
Chat::load_from_db(&t.ctx, chat_id1)
.unwrap()
.get_visibility()
== ChatVisibility::Archived
);
assert!(
Chat::load_from_db(&t.ctx, chat_id2)
.unwrap()
.get_visibility()
== ChatVisibility::Archived
);
assert!(chat_id2.set_archived(&t.ctx, true).is_ok());
assert!(Chat::load_from_db(&t.ctx, chat_id1).unwrap().is_archived());
assert!(Chat::load_from_db(&t.ctx, chat_id2).unwrap().is_archived());
assert_eq!(get_chat_cnt(&t.ctx), 2);
assert_eq!(chatlist_len(&t.ctx, 0), 1); // only DC_CHAT_ID_ARCHIVED_LINK now
assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS), 0);
assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 2);
// archive already archived first chat, unarchive second chat two times
assert!(chat_id1
.set_visibility(&t.ctx, ChatVisibility::Archived)
.is_ok());
assert!(chat_id2
.set_visibility(&t.ctx, ChatVisibility::Normal)
.is_ok());
assert!(chat_id2
.set_visibility(&t.ctx, ChatVisibility::Normal)
.is_ok());
assert!(
Chat::load_from_db(&t.ctx, chat_id1)
.unwrap()
.get_visibility()
== ChatVisibility::Archived
);
assert!(
Chat::load_from_db(&t.ctx, chat_id2)
.unwrap()
.get_visibility()
== ChatVisibility::Normal
);
assert!(chat_id1.set_archived(&t.ctx, true).is_ok());
assert!(chat_id2.set_archived(&t.ctx, false).is_ok());
assert!(chat_id2.set_archived(&t.ctx, false).is_ok());
assert!(Chat::load_from_db(&t.ctx, chat_id1).unwrap().is_archived());
assert!(!Chat::load_from_db(&t.ctx, chat_id2).unwrap().is_archived());
assert_eq!(get_chat_cnt(&t.ctx), 2);
assert_eq!(chatlist_len(&t.ctx, 0), 2);
assert_eq!(chatlist_len(&t.ctx, DC_GCL_NO_SPECIALS), 1);
assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 1);
}
fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec<ChatId> {
let chatlist = Chatlist::try_load(ctx, listflags, None, None).unwrap();
let mut result = Vec::new();
for chatlist_index in 0..chatlist.len() {
result.push(chatlist.get_chat_id(chatlist_index))
}
result
}
#[test]
fn test_pinned() {
let t = dummy_context();
// create 3 chats, wait 1 second in between to get a reliable order (we order by time)
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("foo".to_string());
let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).unwrap();
let chat_id1 = message::Message::load_from_db(&t.ctx, msg_id)
.unwrap()
.chat_id;
std::thread::sleep(std::time::Duration::from_millis(1000));
let chat_id2 = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1000));
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap();
let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS);
assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]);
// pin
assert!(chat_id1
.set_visibility(&t.ctx, ChatVisibility::Pinned)
.is_ok());
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id1)
.unwrap()
.get_visibility(),
ChatVisibility::Pinned
);
// check if chat order changed
let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS);
assert_eq!(chatlist, vec![chat_id1, chat_id3, chat_id2]);
// unpin
assert!(chat_id1
.set_visibility(&t.ctx, ChatVisibility::Normal)
.is_ok());
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id1)
.unwrap()
.get_visibility(),
ChatVisibility::Normal
);
// check if chat order changed back
let chatlist = get_chats_from_chat_list(&t.ctx, DC_GCL_NO_SPECIALS);
assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]);
}
#[test]
fn test_set_chat_name() {
let t = dummy_context();
@@ -3006,49 +2781,4 @@ mod tests {
assert!(chat_id.set_selfavatar_timestamp(&t.ctx, time()).is_ok());
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
}
#[test]
fn test_set_mute_duration() {
let t = dummy_context();
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap();
// Initial
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
// Forever
set_muted(&t.ctx, chat_id, MuteDuration::Forever).unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
true
);
// unMute
set_muted(&t.ctx, chat_id, MuteDuration::NotMuted).unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
// Timed in the future
set_muted(
&t.ctx,
chat_id,
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
)
.unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
true
);
// Time in the past
set_muted(
&t.ctx,
chat_id,
MuteDuration::Until(SystemTime::now() - Duration::from_secs(3600)),
)
.unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
}
}

View File

@@ -60,7 +60,7 @@ impl Chatlist {
/// or "Not now".
/// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
/// archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
/// archived *any* chat using dc_archive_chat(). The UI should show a link as
/// "Show archived chats", if the user clicks this item, the UI should show a
/// list of all archived chats that can be created by this function hen using
/// the DC_GCL_ARCHIVED_ONLY flag.
@@ -127,13 +127,13 @@ impl Chatlist {
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1))
AND (hidden=0 OR state=?))
WHERE c.id>9
AND c.blocked=0
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, query_contact_id as i32],
process_row,
process_rows,
)?
@@ -199,13 +199,13 @@ impl Chatlist {
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1))
AND (hidden=0 OR state=?))
WHERE c.id>9
AND c.blocked=0
AND NOT c.archived=?2
AND c.archived=0
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, ChatVisibility::Archived, ChatVisibility::Pinned],
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft],
process_row,
process_rows,
)?;
@@ -278,8 +278,8 @@ impl Chatlist {
// This is because we may want to display drafts here or stuff as
// "is typing".
// Also, sth. as "No messages" would not work if the summary comes from a message.
let mut ret = Lot::new();
let mut ret = Lot::new();
if index >= self.ids.len() {
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return ret;
@@ -321,10 +321,6 @@ impl Chatlist {
ret
}
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
self.ids.iter().position(|(chat_id, _)| chat_id == &id)
}
}
/// Returns the number of archived chats
@@ -392,9 +388,7 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
assert_eq!(chats.len(), 0);
chat_id1
.set_visibility(&t.ctx, ChatVisibility::Archived)
.ok();
chat_id1.set_archived(&t.ctx, true).ok();
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
assert_eq!(chats.len(), 1);
}

View File

@@ -62,9 +62,6 @@ pub enum Config {
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
ShowEmails,
#[strum(props(default = "0"))]
KeyGenType,
SaveMimeHeaders,
ConfiguredAddr,
ConfiguredMailServer,

View File

@@ -12,13 +12,11 @@ use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::job::{self, job_add, job_kill_action};
use crate::e2ee;
use crate::job;
use crate::login_param::{CertificateChecks, LoginParam};
use crate::oauth2::*;
use crate::param::Params;
use crate::{chat, e2ee, provider};
use crate::message::Message;
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
@@ -32,28 +30,11 @@ macro_rules! progress {
};
}
impl Context {
/// Starts a configuration job.
pub fn configure(&self) {
if self.has_ongoing() {
warn!(self, "There is already another ongoing process running.",);
return;
}
job_kill_action(self, job::Action::ConfigureImap);
job_add(self, job::Action::ConfigureImap, 0, Params::new(), 0);
}
/// Checks if the context is already configured.
pub fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool(self, "configured")
}
}
/*******************************************************************************
* Configure JOB
******************************************************************************/
#[allow(non_snake_case, unused_must_use, clippy::cognitive_complexity)]
pub(crate) fn JobConfigureImap(context: &Context) -> job::Status {
pub fn JobConfigureImap(context: &Context) -> job::Status {
if !context.sql.is_open() {
error!(context, "Cannot configure, database not opened.",);
progress!(context, 0);
@@ -439,77 +420,45 @@ pub(crate) fn JobConfigureImap(context: &Context) -> job::Status {
LoginParam::from_database(context, "configured_raw_").save_to_database(context, "");
}
if let Some(provider) = provider::get_provider_info(&param.addr) {
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(provider.after_login_hint.to_string());
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg)).is_err() {
warn!(context, "cannot add after_login_hint as core-provider-info");
}
}
}
context.free_ongoing();
progress!(context, if success { 1000 } else { 0 });
job::Status::Finished(Ok(()))
}
#[allow(clippy::unnecessary_unwrap)]
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
// XXX we don't have https://github.com/deltachat/provider-db APIs
// integrated yet but we'll already add nauta as a first use case, also
// showing what we need from provider-db in the future.
info!(
context,
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&param.addr) {
match provider.status {
provider::Status::OK | provider::Status::PREPARATION => {
let imap = provider.get_imap_server();
let smtp = provider.get_smtp_server();
// clippy complains about these is_some()/unwrap() settings,
// however, rewriting the code to "if let" would make things less obvious,
// esp. if we allow more combinations of servers (pop, jmap).
// therefore, #[allow(clippy::unnecessary_unwrap)] is added above.
if imap.is_some() && smtp.is_some() {
let imap = imap.unwrap();
let smtp = smtp.unwrap();
if param.addr.ends_with("@nauta.cu") {
let mut p = LoginParam::new();
let mut p = LoginParam::new();
p.addr = param.addr.clone();
p.addr = param.addr.clone();
p.mail_server = "imap.nauta.cu".to_string();
p.mail_user = param.addr.clone();
p.mail_pw = param.mail_pw.clone();
p.mail_port = 143;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.mail_server = imap.hostname.to_string();
p.mail_user = imap.apply_username_pattern(param.addr.clone());
p.mail_port = imap.port as i32;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match imap.socket {
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
};
p.send_server = "smtp.nauta.cu".to_string();
p.send_user = param.addr.clone();
p.send_pw = param.mail_pw.clone();
p.send_port = 25;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags = DC_LP_AUTH_NORMAL as i32
| DC_LP_IMAP_SOCKET_STARTTLS as i32
| DC_LP_SMTP_SOCKET_STARTTLS as i32;
p.send_server = smtp.hostname.to_string();
p.send_user = smtp.apply_username_pattern(param.addr.clone());
p.send_port = smtp.port as i32;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match smtp.socket {
provider::Socket::STARTTLS => DC_LP_SMTP_SOCKET_STARTTLS as i32,
provider::Socket::SSL => DC_LP_SMTP_SOCKET_SSL as i32,
};
info!(context, "offline autoconfig found: {}", p);
return Some(p);
} else {
info!(context, "offline autoconfig found, but no servers defined");
return None;
}
}
provider::Status::BROKEN => {
info!(context, "offline autoconfig found, provider is broken");
return None;
}
}
info!(context, "found offline autoconfig: {}", p);
Some(p)
} else {
info!(context, "no offline autoconfig found");
None
}
info!(context, "no offline autoconfig found");
None
}
fn try_imap_connections(

View File

@@ -3,7 +3,6 @@
use deltachat_derive::*;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
lazy_static! {
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
@@ -16,20 +15,7 @@ const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub enum Blocked {
Not = 0,
@@ -57,19 +43,7 @@ impl Default for ShowEmails {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub enum KeyGenType {
Default = 0,
Rsa2048 = 1,
Ed25519 = 2,
}
impl Default for KeyGenType {
fn default() -> Self {
KeyGenType::Default
}
}
pub const DC_IMAP_SEEN: u32 = 0x1;
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
@@ -116,8 +90,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum Chattype {
@@ -213,20 +185,7 @@ pub const DC_BOB_SUCCESS: i32 = 1;
// max. width/height of an avatar
pub const AVATAR_SIZE: u32 = 192;
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(i32)]
pub enum Viewtype {
Unknown = 0,

View File

@@ -60,7 +60,7 @@ pub struct Contact {
/// to access this field.
authname: String,
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr`` to access this field.
addr: String,
/// Blocked state. Use dc_contact_is_blocked to access this field.
@@ -146,7 +146,7 @@ impl Origin {
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum Modifier {
pub enum Modifier {
None,
Modified,
Created,
@@ -300,31 +300,9 @@ impl Contact {
}
/// Lookup a contact and create it if it does not exist yet.
/// The contact is identified by the email-address, a name and an "origin" can be given.
///
/// The "origin" is where the address comes from -
/// from-header, cc-header, addressbook, qr, manual-edit etc.
/// In general, "better" origins overwrite the names of "worse" origins -
/// Eg. if we got a name in cc-header and later in from-header, the name will change -
/// this does not happen the other way round.
///
/// The "best" origin are manually created contacts -
/// names given manually can only be overwritten by further manual edits
/// (until they are set empty again or reset to the name seen in the From-header).
///
/// These manually edited names are _never_ used for sending on the wire -
/// this should avoid sending sth. as "Mama" or "Daddy" to some 3rd party.
/// Instead, for the wire, we use so called "authnames"
/// that can only be set and updated by a From-header.
///
/// The different names used in the function are:
/// - "name": name passed as function argument, belonging to the given origin
/// - "row_name": current name used in the database, typically set to "name"
/// - "row_authname": name as authorized from a contact, set only through a From-header
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
///
/// Returns the contact_id and a `Modifier` value indicating if a modification occured.
pub(crate) fn add_or_lookup(
pub fn add_or_lookup(
context: &Context,
name: impl AsRef<str>,
addr: impl AsRef<str>,
@@ -378,9 +356,7 @@ impl Contact {
if !name.as_ref().is_empty() {
if !row_name.is_empty() {
if (origin >= row_origin || row_name == row_authname)
&& name.as_ref() != row_name
{
if origin >= row_origin && name.as_ref() != row_name {
update_name = true;
}
} else {
@@ -389,9 +365,6 @@ impl Contact {
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
update_authname = true;
}
} else if origin == Origin::ManuallyCreated && !row_authname.is_empty() {
// no name given on manual edit, this will update the name to the authname
update_name = true;
}
Ok((row_id, row_name, row_addr, row_origin, row_authname))
@@ -402,22 +375,16 @@ impl Contact {
update_addr = true;
}
if update_name || update_authname || update_addr || origin > row_origin {
let new_name = if update_name {
if !name.as_ref().is_empty() {
name.as_ref()
} else {
&row_authname
}
} else {
&row_name
};
sql::execute(
context,
&context.sql,
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
params![
new_name,
if update_name {
name.as_ref()
} else {
&row_name
},
if update_addr { addr } else { &row_addr },
if origin > row_origin {
origin
@@ -435,13 +402,11 @@ impl Contact {
.ok();
if update_name {
// Update the contact name also if it is used as a group name.
// This is one of the few duplicated data, however, getting the chat list is easier this way.
sql::execute(
context,
&context.sql,
"UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
params![new_name, Chattype::Single, row_id]
params![name.as_ref(), Chattype::Single, row_id]
).ok();
}
sth_modified = Modifier::Modified;
@@ -1019,7 +984,7 @@ fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
}
}
pub(crate) fn set_profile_image(
pub fn set_profile_image(
context: &Context,
contact_id: u32,
profile_image: &AvatarAction,
@@ -1036,6 +1001,7 @@ pub(crate) fn set_profile_image(
contact.param.remove(Param::ProfileImage);
true
}
AvatarAction::None => false,
};
if changed {
contact.update_param(context)?;
@@ -1315,7 +1281,6 @@ mod tests {
fn test_remote_authnames() {
let t = dummy_context();
// incoming mail `From: bob1 <bob@example.org>` - this should init authname and name
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob1",
@@ -1330,7 +1295,6 @@ mod tests {
assert_eq!(contact.get_name(), "bob1");
assert_eq!(contact.get_display_name(), "bob1");
// incoming mail `From: bob2 <bob@example.org>` - this should update authname and name
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob2",
@@ -1345,15 +1309,16 @@ mod tests {
assert_eq!(contact.get_name(), "bob2");
assert_eq!(contact.get_display_name(), "bob2");
// manually edit name to "bob3" - authname should be still be "bob2" a given in `From:` above
let contact_id = Contact::create(&t.ctx, "bob3", "bob@example.org").unwrap();
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t.ctx, "bob3", "bob@example.org", Origin::ManuallyCreated)
.unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
// incoming mail `From: bob4 <bob@example.org>` - this should update authname, manually given name is still "bob3"
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob4",
@@ -1369,81 +1334,6 @@ mod tests {
assert_eq!(contact.get_display_name(), "bob3");
}
#[test]
fn test_remote_authnames_create_empty() {
let t = dummy_context();
// manually create "claire@example.org" without a given name
let contact_id = Contact::create(&t.ctx, "", "claire@example.org").unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire@example.org");
// incoming mail `From: claire1 <claire@example.org>` - this should update authname and name
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"claire1",
"claire@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "claire1");
assert_eq!(contact.get_name(), "claire1");
assert_eq!(contact.get_display_name(), "claire1");
// incoming mail `From: claire2 <claire@example.org>` - this should update authname and name
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"claire2",
"claire@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "claire2");
assert_eq!(contact.get_name(), "claire2");
assert_eq!(contact.get_display_name(), "claire2");
}
#[test]
fn test_remote_authnames_edit_empty() {
let t = dummy_context();
// manually create "dave@example.org"
let contact_id = Contact::create(&t.ctx, "dave1", "dave@example.org").unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
// incoming mail `From: dave2 <dave@example.org>` - this should update authname
Contact::add_or_lookup(
&t.ctx,
"dave2",
"dave@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
// manually clear the name
Contact::create(&t.ctx, "", "dave@example.org").unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave2");
assert_eq!(contact.get_display_name(), "dave2");
}
#[test]
fn test_addr_cmp() {
assert!(addr_cmp("AA@AA.ORG", "aa@aa.ORG"));

View File

@@ -3,7 +3,13 @@
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Condvar, Mutex, RwLock,
};
use crossbeam::channel::select;
use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
use crate::chat::*;
use crate::config::Config;
@@ -12,9 +18,9 @@ use crate::contact::*;
use crate::error::*;
use crate::events::Event;
use crate::imap::*;
use crate::job::*;
use crate::job;
use crate::job_thread::JobThread;
use crate::key::Key;
use crate::key::*;
use crate::login_param::LoginParam;
use crate::lot::Lot;
use crate::message::{self, Message, MessengerMessage, MsgId};
@@ -32,7 +38,7 @@ use crate::sql::Sql;
/// * `data2` - Depends on the event parameter, see [Event].
pub type ContextCallback = dyn Fn(&Context, Event) -> () + Send + Sync;
#[derive(DebugStub)]
#[derive(Debug)]
pub struct Context {
/// Database file path
dbfile: PathBuf,
@@ -47,16 +53,21 @@ pub struct Context {
pub smtp: Arc<Mutex<Smtp>>,
pub smtp_state: Arc<(Mutex<SmtpState>, Condvar)>,
pub oauth2_critical: Arc<Mutex<()>>,
#[debug_stub = "Callback"]
cb: Box<ContextCallback>,
pub os_name: Option<String>,
pub cmdline_sel_chat_id: Arc<RwLock<ChatId>>,
pub(crate) bob: Arc<RwLock<BobStatus>>,
pub bob: Arc<RwLock<BobStatus>>,
pub last_smeared_timestamp: RwLock<i64>,
pub running_state: Arc<RwLock<RunningState>>,
/// Mutex to avoid generating the key for the user more than once.
pub generating_key_mutex: Mutex<()>,
pub translated_stockstrings: RwLock<HashMap<usize, String>>,
event_sender: Sender<Event>,
event_receiver: Receiver<Event>,
shutdown_sender: Sender<()>,
shutdown_receiver: Receiver<()>,
is_running: AtomicBool,
}
#[derive(Debug, PartialEq, Eq)]
@@ -80,11 +91,19 @@ pub fn get_info() -> HashMap<&'static str, String> {
res
}
macro_rules! while_running {
($self:expr, $code:block) => {
if $self.is_running.load(Ordering::Relaxed) {
$code
} else {
break;
}
};
}
impl Context {
/// Creates new context.
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> {
pretty_env_logger::try_init_timed().ok();
pub fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
@@ -92,24 +111,22 @@ impl Context {
if !blobdir.exists() {
std::fs::create_dir_all(&blobdir)?;
}
Context::with_blobdir(cb, os_name, dbfile, blobdir)
Context::with_blobdir(os_name, dbfile, blobdir)
}
pub fn with_blobdir(
cb: Box<ContextCallback>,
os_name: String,
dbfile: PathBuf,
blobdir: PathBuf,
) -> Result<Context> {
pub fn with_blobdir(os_name: String, dbfile: PathBuf, blobdir: PathBuf) -> Result<Context> {
ensure!(
blobdir.is_dir(),
"Blobdir does not exist: {}",
blobdir.display()
);
let (event_sender, event_receiver) = unbounded();
let (shutdown_sender, shutdown_receiver) = bounded(0);
let ctx = Context {
blobdir,
dbfile,
cb,
os_name: Some(os_name),
running_state: Arc::new(RwLock::new(Default::default())),
sql: Sql::new(),
@@ -138,6 +155,11 @@ impl Context {
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
generating_key_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
event_sender,
event_receiver,
shutdown_sender,
shutdown_receiver,
is_running: Default::default(),
};
ensure!(
@@ -159,7 +181,98 @@ impl Context {
}
pub fn call_cb(&self, event: Event) {
(*self.cb)(self, event);
self.event_sender.send(event).unwrap();
}
/// Start the run loop.
pub fn run<F>(&self, cb: F)
where
F: Fn(&Context, Event) -> () + Send + Sync,
{
if self.is_running.swap(true, Ordering::Relaxed) {
panic!("Run can only be called once");
}
crossbeam::scope(|s| {
let imap_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_inbox_jobs(self);
while_running!(self, {
job::perform_inbox_fetch(self);
while_running!(self, { job::perform_inbox_idle(self) });
});
});
});
let mvbox_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_mvbox_fetch(self);
while_running!(self, {
job::perform_mvbox_idle(self);
});
});
});
let sentbox_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_sentbox_fetch(self);
while_running!(self, {
job::perform_sentbox_idle(self);
});
});
});
let smtp_handle = s.spawn(|_| loop {
while_running!(self, {
job::perform_smtp_jobs(self);
while_running!(self, {
job::perform_smtp_idle(self);
});
});
});
loop {
select! {
recv(self.event_receiver) -> event => {
// This gurantees that the callback is always called from the thread
// that called `run`.
cb(self, event.unwrap())
},
recv(self.shutdown_receiver) -> _ => break,
}
}
imap_handle.join().unwrap();
mvbox_handle.join().unwrap();
sentbox_handle.join().unwrap();
smtp_handle.join().unwrap();
})
.unwrap()
}
/// Stop the run loop. Blocks until all threads have shutdown.
pub fn shutdown(&self) {
if !self.is_running.swap(false, Ordering::Relaxed) {
// already shutdown
return;
}
self.shutdown_sender.send(()).unwrap();
job::interrupt_inbox_idle(self);
job::interrupt_mvbox_idle(self);
job::interrupt_sentbox_idle(self);
job::interrupt_smtp_idle(self);
}
pub fn configure(&self) {
if self.has_ongoing() {
warn!(self, "There is already another ongoing process running.");
return;
}
job::job_kill_action(self, job::Action::ConfigureImap);
job::job_add(self, job::Action::ConfigureImap, 0, Params::new(), 0);
}
/// Check if the context is already configured.
pub fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool(self, "configured")
}
/*******************************************************************************
@@ -441,9 +554,9 @@ impl Context {
match msg.is_dc_message {
MessengerMessage::No => {}
MessengerMessage::Yes | MessengerMessage::Reply => {
job_add(
job::job_add(
self,
Action::MoveMsg,
job::Action::MoveMsg,
msg.id.to_u32() as i32,
Params::new(),
0,
@@ -478,14 +591,14 @@ impl Default for RunningState {
}
#[derive(Debug, Default)]
pub(crate) struct BobStatus {
pub struct BobStatus {
pub expects: i32,
pub status: i32,
pub qr_scan: Option<Lot>,
}
#[derive(Debug, PartialEq)]
pub(crate) enum PerformJobsNeeded {
pub enum PerformJobsNeeded {
Not,
AtOnce,
AvoidDos,
@@ -502,7 +615,7 @@ pub struct SmtpState {
pub idle: bool,
pub suspended: bool,
pub doing_jobs: bool,
pub(crate) perform_jobs_needed: PerformJobsNeeded,
pub perform_jobs_needed: PerformJobsNeeded,
pub probe_network: bool,
}
@@ -521,7 +634,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new(Box::new(|_, _| ()), "FakeOs".into(), dbfile);
let res = Context::new("FakeOs".into(), dbfile);
assert!(res.is_err());
}
@@ -536,7 +649,7 @@ mod tests {
fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap();
Context::new("FakeOS".into(), dbfile).unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir());
}
@@ -547,7 +660,7 @@ mod tests {
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile);
let res = Context::new("FakeOS".into(), dbfile);
assert!(res.is_err());
}
@@ -557,7 +670,7 @@ mod tests {
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap();
Context::new("FakeOS".into(), dbfile).unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
@@ -567,7 +680,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir("FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}
@@ -576,7 +689,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir("FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}

View File

@@ -37,7 +37,7 @@ pub fn dc_receive_imf(
imf_raw: &[u8],
server_folder: impl AsRef<str>,
server_uid: u32,
seen: bool,
flags: u32,
) -> Result<()> {
info!(
context,
@@ -74,12 +74,18 @@ pub fn dc_receive_imf(
// helper method to handle early exit and memory cleanup
let cleanup = |context: &Context,
create_event_to_send: &Option<CreateEvent>,
created_db_entries: Vec<(ChatId, MsgId)>| {
created_db_entries: &Vec<(ChatId, MsgId)>| {
if let Some(create_event_to_send) = create_event_to_send {
for (chat_id, msg_id) in created_db_entries {
let event = match create_event_to_send {
CreateEvent::MsgsChanged => Event::MsgsChanged { msg_id, chat_id },
CreateEvent::IncomingMsg => Event::IncomingMsg { msg_id, chat_id },
CreateEvent::MsgsChanged => Event::MsgsChanged {
msg_id: *msg_id,
chat_id: *chat_id,
},
CreateEvent::IncomingMsg => Event::IncomingMsg {
msg_id: *msg_id,
chat_id: *chat_id,
},
};
context.call_cb(event);
}
@@ -94,15 +100,41 @@ pub fn dc_receive_imf(
// get From: (it can be an address list!) and check if it is known (for known From:'s we add
// the other To:/Cc: in the 3rd pass)
// or if From: is equal to SELF (in this case, it is any outgoing messages,
// we do not check Return-Path any more as this is unreliable, see
// https://github.com/deltachat/deltachat-core/issues/150)
let (from_id, from_id_blocked, incoming_origin) =
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
from_field_to_contact_id(context, field_from)?
// we do not check Return-Path any more as this is unreliable, see issue #150)
let mut from_id = 0;
let mut from_id_blocked = false;
let mut incoming = true;
let mut incoming_origin = Origin::Unknown;
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
let from_ids = dc_add_or_lookup_contacts_by_address_list(
context,
&field_from,
Origin::IncomingUnknownFrom,
)?;
if from_ids.contains(&DC_CONTACT_ID_SELF) {
incoming = false;
from_id = DC_CONTACT_ID_SELF;
incoming_origin = Origin::OutgoingBcc;
} else if !from_ids.is_empty() {
if from_ids.len() > 1 {
warn!(
context,
"mail has more than one From address, only using first: {:?}", field_from
);
}
from_id = from_ids.get_index(0).cloned().unwrap_or_default();
if let Ok(contact) = Contact::load_from_db(context, from_id) {
incoming_origin = contact.origin;
from_id_blocked = contact.blocked;
}
} else {
(0, false, Origin::Unknown)
};
let incoming = from_id != DC_CONTACT_ID_SELF;
warn!(context, "mail has an empty From header: {:?}", field_from);
// if there is no from given, from_id stays 0 which is just fine. These messages
// are very rare, however, we have to add them to the database (they go to the
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
}
}
let mut to_ids = ContactIds::new();
for header_def in &[HeaderDef::To, HeaderDef::Cc] {
@@ -153,13 +185,13 @@ pub fn dc_receive_imf(
from_id_blocked,
&mut hidden,
&mut chat_id,
seen,
flags,
&mut needs_delete_job,
&mut insert_msg_id,
&mut created_db_entries,
&mut create_event_to_send,
) {
cleanup(context, &create_event_to_send, created_db_entries);
cleanup(context, &create_event_to_send, &created_db_entries);
bail!("add_parts error: {:?}", err);
}
} else {
@@ -181,8 +213,8 @@ pub fn dc_receive_imf(
);
}
if let Some(avatar_action) = &mime_parser.user_avatar {
match contact::set_profile_image(&context, from_id, avatar_action) {
if mime_parser.user_avatar != AvatarAction::None {
match contact::set_profile_image(&context, from_id, &mime_parser.user_avatar) {
Ok(()) => {
context.call_cb(Event::ChatModified(chat_id));
}
@@ -210,54 +242,13 @@ pub fn dc_receive_imf(
"received message {} has Message-Id: {}", server_uid, rfc724_mid
);
cleanup(context, &create_event_to_send, created_db_entries);
cleanup(context, &create_event_to_send, &created_db_entries);
mime_parser.handle_reports(context, from_id, sent_timestamp, &server_folder, server_uid);
mime_parser.handle_reports(from_id, sent_timestamp, &server_folder, server_uid);
Ok(())
}
/// Converts "From" field to contact id.
///
/// Also returns whether it is blocked or not and its origin.
pub fn from_field_to_contact_id(
context: &Context,
field_from: &str,
) -> Result<(u32, bool, Origin)> {
let from_ids = dc_add_or_lookup_contacts_by_address_list(
context,
&field_from,
Origin::IncomingUnknownFrom,
)?;
if from_ids.contains(&DC_CONTACT_ID_SELF) {
Ok((DC_CONTACT_ID_SELF, false, Origin::OutgoingBcc))
} else if !from_ids.is_empty() {
if from_ids.len() > 1 {
warn!(
context,
"mail has more than one From address, only using first: {:?}", field_from
);
}
let from_id = from_ids.get_index(0).cloned().unwrap_or_default();
let mut from_id_blocked = false;
let mut incoming_origin = Origin::Unknown;
if let Ok(contact) = Contact::load_from_db(context, from_id) {
from_id_blocked = contact.blocked;
incoming_origin = contact.origin;
}
Ok((from_id, from_id_blocked, incoming_origin))
} else {
warn!(context, "mail has an empty From header: {:?}", field_from);
// if there is no from given, from_id stays 0 which is just fine. These messages
// are very rare, however, we have to add them to the database (they go to the
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
Ok((0, false, Origin::Unknown))
}
}
#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
fn add_parts(
context: &Context,
@@ -274,7 +265,7 @@ fn add_parts(
from_id_blocked: bool,
hidden: &mut bool,
chat_id: &mut ChatId,
seen: bool,
flags: u32,
needs_delete_job: &mut bool,
insert_msg_id: &mut MsgId,
created_db_entries: &mut Vec<(ChatId, MsgId)>,
@@ -308,7 +299,8 @@ fn add_parts(
} else {
MessengerMessage::No
};
// incoming non-chat messages may be discarded
// incoming non-chat messages may be discarded;
// maybe this can be optimized later, by checking the state before the message body is downloaded
let mut allow_creation = true;
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default();
@@ -316,13 +308,11 @@ fn add_parts(
&& msgrmsg == MessengerMessage::No
{
// this message is a classic email not a chat-message nor a reply to one
match show_emails {
ShowEmails::Off => {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
allow_creation = false;
}
ShowEmails::AcceptedContacts => allow_creation = false,
ShowEmails::All => {}
if show_emails == ShowEmails::Off {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
allow_creation = false
} else if show_emails == ShowEmails::AcceptedContacts {
allow_creation = false
}
}
@@ -333,7 +323,7 @@ fn add_parts(
let to_id: u32;
if incoming {
state = if seen {
state = if 0 != flags & DC_IMAP_SEEN {
MessageState::InSeen
} else {
MessageState::InFresh
@@ -518,7 +508,7 @@ fn add_parts(
if chat_id.is_unset() && self_sent {
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
// maybe an Autocrypt Setup Message
// maybe an Autocrypt Setup Messag
let (id, bl) =
chat::create_or_lookup_by_contact_id(context, DC_CONTACT_ID_SELF, Blocked::Not)
.unwrap_or_default();
@@ -541,7 +531,7 @@ fn add_parts(
*chat_id,
from_id,
*sent_timestamp,
!seen,
0 == flags & DC_IMAP_SEEN,
&mut sort_timestamp,
sent_timestamp,
&mut rcvd_timestamp,
@@ -860,21 +850,21 @@ fn create_or_lookup_group(
mime_parser.is_system_message = SystemMessage::GroupNameChanged;
} else if let Some(value) = mime_parser.get(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
mime_parser.is_system_message = SystemMessage::GroupImageChanged;
better_msg = context.stock_system_msg(
match avatar_action {
AvatarAction::Delete => StockMessage::MsgGrpImgDeleted,
AvatarAction::Change(_) => StockMessage::MsgGrpImgChanged,
},
"",
"",
from_id as u32,
)
}
if value == "group-avatar-changed" && mime_parser.group_avatar != AvatarAction::None
{
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
mime_parser.is_system_message = SystemMessage::GroupImageChanged;
better_msg = context.stock_system_msg(
if mime_parser.group_avatar == AvatarAction::Delete {
StockMessage::MsgGrpImgDeleted
} else {
StockMessage::MsgGrpImgChanged
},
"",
"",
from_id as u32,
)
}
}
}
@@ -985,7 +975,7 @@ fn create_or_lookup_group(
// XXX insert code in a different PR :)
// execute group commands
if X_MrAddToGrp.is_some() {
if X_MrAddToGrp.is_some() || X_MrRemoveFromGrp.is_some() {
recreate_member_list = true;
} else if X_MrGrpNameChanged {
if let Some(ref grpname) = grpname {
@@ -1004,16 +994,17 @@ fn create_or_lookup_group(
}
}
}
if let Some(avatar_action) = &mime_parser.group_avatar {
if mime_parser.group_avatar != AvatarAction::None {
info!(context, "group-avatar change for {}", chat_id);
if let Ok(mut chat) = Chat::load_from_db(context, chat_id) {
match avatar_action {
match &mime_parser.group_avatar {
AvatarAction::Change(profile_image) => {
chat.param.set(Param::ProfileImage, profile_image);
}
AvatarAction::Delete => {
chat.param.remove(Param::ProfileImage);
}
AvatarAction::None => {}
};
chat.update_param(context)?;
send_EVENT_CHAT_MODIFIED = true;
@@ -1021,32 +1012,39 @@ fn create_or_lookup_group(
}
// add members to group/check members
// for recreation: we should add a timestamp
if recreate_member_list {
if !chat::is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) {
// TODO: the member list should only be recreated if the corresponding message is newer
// than the one that is responsible for the current member list, see
// https://github.com/deltachat/deltachat-core/issues/127
let skip = X_MrRemoveFromGrp.as_ref();
sql::execute(
context,
&context.sql,
"DELETE FROM chats_contacts WHERE chat_id=?;",
params![chat_id],
)
.ok();
if skip.is_none() || !addr_cmp(&self_addr, skip.unwrap()) {
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF);
}
if from_id > DC_CONTACT_ID_LAST_SPECIAL
if from_id > DC_CHAT_ID_LAST_SPECIAL
&& !Contact::addr_equals_contact(context, &self_addr, from_id as u32)
&& !chat::is_contact_in_chat(context, chat_id, from_id)
&& (skip.is_none()
|| !Contact::addr_equals_contact(context, skip.unwrap(), from_id as u32))
{
chat::add_to_chat_contacts_table(context, chat_id, from_id as u32);
}
for &to_id in to_ids.iter() {
info!(context, "adding to={:?} to chat id={}", to_id, chat_id);
if !Contact::addr_equals_contact(context, &self_addr, to_id)
&& !chat::is_contact_in_chat(context, chat_id, to_id)
&& (skip.is_none() || !Contact::addr_equals_contact(context, skip.unwrap(), to_id))
{
chat::add_to_chat_contacts_table(context, chat_id, to_id);
}
}
send_EVENT_CHAT_MODIFIED = true;
} else if let Some(removed_addr) = X_MrRemoveFromGrp {
let contact_id = Contact::lookup_id_by_addr(context, removed_addr);
if contact_id != 0 {
info!(context, "remove {:?} from chat id={}", contact_id, chat_id);
chat::remove_from_chat_contacts_table(context, chat_id, contact_id);
}
send_EVENT_CHAT_MODIFIED = true;
}
if send_EVENT_CHAT_MODIFIED {
@@ -1056,7 +1054,7 @@ fn create_or_lookup_group(
}
/// try extract a grpid from a message-id list header value
fn extract_grpid(mime_parser: &MimeMessage, headerdef: HeaderDef) -> Option<&str> {
fn extract_grpid<'a>(mime_parser: &'a MimeMessage, headerdef: HeaderDef) -> Option<&'a str> {
let header = mime_parser.get(headerdef)?;
let parts = header
.split(',')
@@ -1464,7 +1462,7 @@ fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
return false;
}
if let Ok(ids) = mailparse::msgidparse(mid_list) {
if let Ok(ids) = mailparse::addrparse(mid_list) {
for id in ids.iter() {
if is_known_rfc724_mid(context, id) {
return true;
@@ -1476,7 +1474,8 @@ fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
}
/// Check if a message is a reply to a known message (messenger or non-messenger).
fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
fn is_known_rfc724_mid(context: &Context, rfc724_mid: &mailparse::MailAddr) -> bool {
let addr = extract_single_from_addr(rfc724_mid);
context
.sql
.exists(
@@ -1484,7 +1483,7 @@ fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
LEFT JOIN chats c ON m.chat_id=c.id \
WHERE m.rfc724_mid=? \
AND m.chat_id>9 AND c.blocked=0;",
params![rfc724_mid],
params![addr],
)
.unwrap_or_default()
}
@@ -1510,8 +1509,8 @@ fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) -
false
}
pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
if let Ok(ids) = mailparse::msgidparse(mid_list) {
fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
if let Ok(ids) = mailparse::addrparse(mid_list) {
for id in ids.iter() {
if is_msgrmsg_rfc724_mid(context, id) {
return true;
@@ -1521,13 +1520,21 @@ pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -
false
}
fn extract_single_from_addr(addr: &mailparse::MailAddr) -> &String {
match addr {
mailparse::MailAddr::Group(infos) => &infos.addrs[0].addr,
mailparse::MailAddr::Single(info) => &info.addr,
}
}
/// Check if a message is a reply to any messenger message.
fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &mailparse::MailAddr) -> bool {
let addr = extract_single_from_addr(rfc724_mid);
context
.sql
.exists(
"SELECT id FROM msgs WHERE rfc724_mid=? AND msgrmsg!=0 AND chat_id>9;",
params![rfc724_mid],
params![addr],
)
.unwrap_or_default()
}

View File

@@ -460,7 +460,7 @@ pub(crate) fn time() -> i64 {
///
/// ```
/// use deltachat::dc_tools::EmailAddress;
/// let email = match EmailAddress::new("someone@example.com") {
/// let email = match "someone@example.com".parse::<EmailAddress>() {
/// Ok(addr) => addr,
/// Err(e) => panic!("Error parsing address, error was {}", e),
/// };
@@ -468,18 +468,12 @@ pub(crate) fn time() -> i64 {
/// assert_eq!(&email.domain, "example.com");
/// assert_eq!(email.to_string(), "someone@example.com");
/// ```
#[derive(Debug, PartialEq, Eq, Clone)]
#[derive(Debug, PartialEq, Clone)]
pub struct EmailAddress {
pub local: String,
pub domain: String,
}
impl EmailAddress {
pub fn new(input: &str) -> Result<Self, Error> {
input.parse::<EmailAddress>()
}
}
impl fmt::Display for EmailAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}@{}", self.local, self.domain)

View File

@@ -1,19 +1,15 @@
//! End-to-end encryption support.
use std::collections::HashSet;
use std::convert::TryFrom;
use mailparse::ParsedMail;
use mailparse::{MailHeaderMap, ParsedMail};
use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::config::Config;
use crate::constants::KeyGenType;
use crate::context::Context;
use crate::dc_tools::EmailAddress;
use crate::error::*;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{self, Key, KeyPairUse, SignedPublicKey};
use crate::key::*;
use crate::keyring::*;
use crate::peerstate::*;
use crate::pgp;
@@ -23,7 +19,7 @@ use crate::securejoin::handle_degrade_event;
pub struct EncryptHelper {
pub prefer_encrypt: EncryptPreference,
pub addr: String,
pub public_key: SignedPublicKey,
pub public_key: Key,
}
impl EncryptHelper {
@@ -106,8 +102,8 @@ impl EncryptHelper {
})?;
keyring.add_ref(key);
}
let public_key = Key::from(self.public_key.clone());
keyring.add_ref(&public_key);
keyring.add_ref(&self.public_key);
let sign_key = Key::from_self_private(context, self.addr.clone(), &context.sql)
.ok_or_else(|| format_err!("missing own private key"))?;
@@ -126,7 +122,7 @@ pub fn try_decrypt(
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
let from = mail
.headers
.get_header_value(HeaderDef::From_)?
.get_first_value("From")?
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
@@ -195,35 +191,44 @@ pub fn try_decrypt(
/// storing a new one when one doesn't exist yet. Care is taken to
/// only generate one key per context even when multiple threads call
/// this function concurrently.
fn load_or_generate_self_public_key(
context: &Context,
self_addr: impl AsRef<str>,
) -> Result<SignedPublicKey> {
fn load_or_generate_self_public_key(context: &Context, self_addr: impl AsRef<str>) -> Result<Key> {
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
return SignedPublicKey::try_from(key)
.map_err(|_| Error::Message("Not a public key".into()));
return Ok(key);
}
let _guard = context.generating_key_mutex.lock().unwrap();
// Check again in case the key was generated while we were waiting for the lock.
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
return SignedPublicKey::try_from(key)
.map_err(|_| Error::Message("Not a public key".into()));
return Ok(key);
}
let start = std::time::Instant::now();
let keygen_type =
KeyGenType::from_i32(context.get_config_int(Config::KeyGenType)).unwrap_or_default();
info!(context, "Generating keypair with type {}", keygen_type);
let keypair = pgp::create_keypair(EmailAddress::new(self_addr.as_ref())?, keygen_type)?;
key::store_self_keypair(context, &keypair, KeyPairUse::Default)?;
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().as_secs()
"Generating keypair with {} bits, e={} ...", 2048, 65537,
);
Ok(keypair.public)
match pgp::create_keypair(&self_addr) {
Some((public_key, private_key)) => {
if dc_key_save_self_keypair(
context,
&public_key,
&private_key,
&self_addr,
true,
&context.sql,
) {
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().as_secs()
);
Ok(public_key)
} else {
Err(format_err!("Failed to save keypair"))
}
}
None => Err(format_err!("Failed to generate keypair")),
}
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.

View File

@@ -16,9 +16,6 @@ pub enum Error {
#[fail(display = "{:?}", _0)]
Message(String),
#[fail(display = "{:?}", _0)]
MessageWithCause(String, #[cause] failure::Error, failure::Backtrace),
#[fail(display = "{:?}", _0)]
Image(image_meta::ImageError),
@@ -121,18 +118,6 @@ impl From<crate::message::InvalidMsgId> for Error {
}
}
impl From<crate::key::SaveKeyError> for Error {
fn from(err: crate::key::SaveKeyError) -> Error {
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
}
}
impl From<crate::pgp::PgpKeygenError> for Error {
fn from(err: crate::pgp::PgpKeygenError) -> Error {
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
}
}
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::MailParseError(err)

View File

@@ -1,7 +1,4 @@
use crate::strum::AsStaticRef;
use mailparse::{MailHeader, MailHeaderMap, MailParseError};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames)]
#[strum(serialize_all = "kebab_case")]
#[allow(dead_code)]
pub enum HeaderDef {
@@ -26,6 +23,7 @@ pub enum HeaderDef {
ChatGroupName,
ChatGroupNameChanged,
ChatVerified,
ChatGroupImage, // deprecated
ChatGroupAvatar,
ChatUserAvatar,
ChatVoiceMessage,
@@ -34,7 +32,6 @@ pub enum HeaderDef {
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
SecureJoinGroup,
@@ -46,18 +43,8 @@ pub enum HeaderDef {
impl HeaderDef {
/// Returns the corresponding Event id.
pub fn get_headername(&self) -> &'static str {
self.as_static()
}
}
pub trait HeaderDefMap {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError>;
}
impl HeaderDefMap for [MailHeader<'_>] {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError> {
self.get_first_value(headerdef.get_headername())
pub fn get_headername(&self) -> String {
self.to_string()
}
}
@@ -68,29 +55,8 @@ mod tests {
#[test]
/// Test that kebab_case serialization works as expected
fn kebab_test() {
assert_eq!(HeaderDef::From_.get_headername(), "from");
assert_eq!(HeaderDef::From_.to_string(), "from");
assert_eq!(HeaderDef::_TestHeader.get_headername(), "test-header");
}
#[test]
/// Test that headers are parsed case-insensitively
fn test_get_header_value_case() {
let (headers, _) =
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
assert_eq!(
headers
.get_header_value(HeaderDef::AutocryptSetupMessage)
.unwrap(),
Some("v99".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::From_).unwrap(),
Some("Bob".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::Autocrypt).unwrap(),
None
);
assert_eq!(HeaderDef::_TestHeader.to_string(), "test-header");
}
}

View File

@@ -1,104 +0,0 @@
use async_imap::{
error::{Error as ImapError, Result as ImapResult},
Client as ImapClient,
};
use async_native_tls::TlsStream;
use async_std::net::{self, TcpStream};
use super::session::Session;
use crate::login_param::{dc_build_tls, CertificateChecks};
#[derive(Debug)]
pub(crate) enum Client {
Secure(ImapClient<TlsStream<TcpStream>>),
Insecure(ImapClient<TcpStream>),
}
impl Client {
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(certificate_checks);
let tls_stream = tls.connect(domain.as_ref(), stream).await?;
let mut client = ImapClient::new(tls_stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Secure(client))
}
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let mut client = ImapClient::new(stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Insecure(client))
}
pub async fn secure<S: AsRef<str>>(
self,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Client> {
match self {
Client::Insecure(client) => {
let tls = dc_build_tls(certificate_checks);
let client_sec = client.secure(domain, tls).await?;
Ok(Client::Secure(client_sec))
}
// Nothing to do
Client::Secure(_) => Ok(self),
}
}
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
self,
auth_type: S,
authenticator: &A,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
}
}
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
self,
username: U,
password: P,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
}
}
}

View File

@@ -1,17 +1,15 @@
use super::Imap;
use async_imap::extensions::idle::{Handle as ImapIdleHandle, IdleResponse};
use async_native_tls::TlsStream;
use async_std::net::TcpStream;
use async_imap::extensions::idle::IdleResponse;
use async_std::prelude::*;
use async_std::task;
use std::sync::atomic::Ordering;
use std::time::{Duration, SystemTime};
use crate::context::Context;
use crate::imap_client::*;
use super::select_folder;
use super::session::Session;
type Result<T> = std::result::Result<T, Error>;
@@ -29,6 +27,9 @@ pub enum Error {
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[fail(display = "IMAP error")]
ImapError(#[cause] async_imap::error::Error),
#[fail(display = "Setup handle error")]
SetupHandleError(#[cause] super::Error),
}
@@ -39,27 +40,6 @@ impl From<select_folder::Error> for Error {
}
}
#[derive(Debug)]
pub(crate) enum IdleHandle {
Secure(ImapIdleHandle<TlsStream<TcpStream>>),
Insecure(ImapIdleHandle<TcpStream>),
}
impl Session {
pub fn idle(self) -> IdleHandle {
match self {
Session::Secure(i) => {
let h = i.idle();
IdleHandle::Secure(h)
}
Session::Insecure(i) => {
let h = i.idle();
IdleHandle::Insecure(h)
}
}
}
}
impl Imap {
pub fn can_idle(&self) -> bool {
task::block_on(async move { self.config.read().await.can_idle })

View File

@@ -5,8 +5,6 @@
use std::sync::atomic::{AtomicBool, Ordering};
use num_traits::FromPrimitive;
use async_imap::{
error::Result as ImapResult,
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
@@ -14,14 +12,11 @@ use async_imap::{
use async_std::sync::{Mutex, RwLock};
use async_std::task;
use crate::config::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_receive_imf::{
dc_receive_imf, from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list,
};
use crate::dc_receive_imf::dc_receive_imf;
use crate::events::Event;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap_client::*;
use crate::job::{job_add, Action};
use crate::login_param::{CertificateChecks, LoginParam};
use crate::message::{self, update_server_uid};
@@ -29,13 +24,10 @@ use crate::oauth2::dc_get_oauth2_access_token;
use crate::param::Params;
use crate::stock::StockMessage;
mod client;
mod idle;
pub mod select_folder;
mod session;
use client::Client;
use session::Session;
const DC_IMAP_SEEN: usize = 0x0001;
type Result<T> = std::result::Result<T, Error>;
@@ -71,9 +63,6 @@ pub enum Error {
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[fail(display = "Mail parse error")]
MailParseError(#[cause] mailparse::MailParseError),
#[fail(display = "No mailbox selected, folder: {:?}", _0)]
NoMailbox(String),
@@ -105,12 +94,6 @@ impl From<select_folder::Error> for Error {
}
}
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::MailParseError(err)
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
pub enum ImapActionResult {
Failed,
@@ -119,20 +102,7 @@ pub enum ImapActionResult {
Success,
}
/// Prefetch:
/// - Message-ID to check if we already have the message.
/// - In-Reply-To and References to check if message is a reply to chat message.
/// - Chat-Version to check if a message is a chat message
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
/// not necessarily sent by Delta Chat.
const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
FROM \
IN-REPLY-TO REFERENCES \
CHAT-VERSION \
AUTOCRYPT-SETUP-MESSAGE\
)])";
const DELETE_CHECK_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
const PREFETCH_FLAGS: &str = "(UID ENVELOPE)";
const JUST_UID: &str = "(UID)";
const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])";
const SELECT_ALL: &str = "1:*";
@@ -184,10 +154,7 @@ struct ImapConfig {
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
pub can_idle: bool,
/// True if the server has MOVE capability as defined in
/// https://tools.ietf.org/html/rfc6851
pub can_move: bool,
pub has_xlist: bool,
pub imap_delimiter: char,
}
@@ -205,7 +172,7 @@ impl Default for ImapConfig {
selected_mailbox: None,
selected_folder_needs_expunge: false,
can_idle: false,
can_move: false,
has_xlist: false,
imap_delimiter: '.',
}
}
@@ -359,7 +326,7 @@ impl Imap {
cfg.imap_port = 0;
cfg.can_idle = false;
cfg.can_move = false;
cfg.has_xlist = false;
}
/// Connects to imap account using already-configured parameters.
@@ -420,7 +387,7 @@ impl Imap {
true
} else {
let can_idle = caps.has_str("IDLE");
let can_move = caps.has_str("MOVE");
let has_xlist = caps.has_str("XLIST");
let caps_list = caps.iter().fold(String::new(), |s, c| {
if let Capability::Atom(x) = c {
s + &format!(" {}", x)
@@ -430,7 +397,7 @@ impl Imap {
});
self.config.write().await.can_idle = can_idle;
self.config.write().await.can_move = can_move;
self.config.write().await.has_xlist = has_xlist;
*self.connected.lock().await = true;
emit_event!(
context,
@@ -588,9 +555,6 @@ impl Imap {
context: &Context,
folder: S,
) -> Result<bool> {
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default();
let (uid_validity, last_seen_uid) =
self.select_with_uidvalidity(context, folder.as_ref())?;
@@ -598,7 +562,7 @@ impl Imap {
let mut list = if let Some(ref mut session) = &mut *self.session.lock().await {
// fetch messages with larger UID than the last one seen
// `(UID FETCH lastseenuid+1:*)`, see RFC 4549
// (`UID FETCH lastseenuid+1:*)`, see RFC 4549
let set = format!("{}:*", last_seen_uid + 1);
match session.uid_fetch(set, PREFETCH_FLAGS).await {
Ok(list) => list,
@@ -616,16 +580,11 @@ impl Imap {
list.sort_unstable_by_key(|msg| msg.uid.unwrap_or_default());
for fetch in &list {
let cur_uid = fetch.uid.unwrap_or_default();
for msg in &list {
let cur_uid = msg.uid.unwrap_or_default();
if cur_uid <= last_seen_uid {
// If the mailbox is not empty, results always include
// at least one UID, even if last_seen_uid+1 is past
// the last UID in the mailbox. It happens because
// uid+1:* is interpreted the same way as *:uid+1.
// See https://tools.ietf.org/html/rfc3501#page-61 for
// standard reference. Therefore, sometimes we receive
// already seen messages and have to filter them out.
// seems that at least dovecot sends the last available UID
// even if we asked for higher UID+N:*
info!(
context,
"fetch_new_messages: ignoring uid {}, last seen was {}", cur_uid, last_seen_uid
@@ -634,9 +593,20 @@ impl Imap {
}
read_cnt += 1;
let headers = get_fetch_headers(fetch)?;
let message_id = prefetch_get_message_id(&headers).unwrap_or_default();
if precheck_imf(context, &message_id, folder.as_ref(), cur_uid) {
let message_id = prefetch_get_message_id(msg).unwrap_or_default();
if !precheck_imf(context, &message_id, folder.as_ref(), cur_uid) {
// check passed, go fetch the rest
if self.fetch_single_msg(context, &folder, cur_uid).await == 0 {
info!(
context,
"Read error for message {} from \"{}\", trying over later.",
message_id,
folder.as_ref()
);
read_errors += 1;
}
} else {
// we know the message-id already or don't want the message otherwise.
info!(
context,
@@ -644,34 +614,6 @@ impl Imap {
message_id,
folder.as_ref(),
);
} else {
let show = prefetch_should_download(context, &headers, show_emails)
.map_err(|err| {
warn!(context, "prefetch_should_download error: {}", err);
err
})
.unwrap_or(true);
if !show {
info!(
context,
"Ignoring new message {} from \"{}\".",
message_id,
folder.as_ref(),
);
} else {
// check passed, go fetch the rest
if let Err(err) = self.fetch_single_msg(context, &folder, cur_uid).await {
info!(
context,
"Read error for message {} from \"{}\", trying over later: {}.",
message_id,
folder.as_ref(),
err
);
read_errors += 1;
}
}
}
if read_errors == 0 {
new_last_seen_uid = cur_uid;
@@ -715,19 +657,17 @@ impl Imap {
context.sql.set_raw_config(context, &key, Some(&val)).ok();
}
/// Fetches a single message by server UID.
///
/// If it succeeds, the message should be treated as received even
/// if no database entries are created. If the function returns an
/// error, the caller should try again later.
async fn fetch_single_msg<S: AsRef<str>>(
&self,
context: &Context,
folder: S,
server_uid: u32,
) -> Result<()> {
) -> usize {
// the function returns:
// 0 the caller should try over again later
// or 1 if the messages should be treated as received, the caller should not try to read the message again (even if no database entries are returned)
if !self.is_connected().await {
return Err(Error::Other("Not connected".to_string()));
return 0;
}
let set = format!("{}", server_uid);
@@ -746,24 +686,41 @@ impl Imap {
folder.as_ref(),
err
);
return Err(Error::FetchFailed(err));
return 0;
}
}
} else {
// we could not get a valid imap session, this should be retried
self.trigger_reconnect();
return Err(Error::Other("Could not get IMAP session".to_string()));
return 0;
};
if let Some(msg) = msgs.first() {
if msgs.is_empty() {
warn!(
context,
"Message #{} does not exist in folder \"{}\".",
server_uid,
folder.as_ref()
);
} else {
let msg = &msgs[0];
// XXX put flags into a set and pass them to dc_receive_imf
let is_deleted = msg.flags().any(|flag| flag == Flag::Deleted);
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
let is_deleted = msg.flags().any(|flag| match flag {
Flag::Deleted => true,
_ => false,
});
let is_seen = msg.flags().any(|flag| match flag {
Flag::Seen => true,
_ => false,
});
let flags = if is_seen { DC_IMAP_SEEN } else { 0 };
if !is_deleted && msg.body().is_some() {
let body = msg.body().unwrap_or_default();
if let Err(err) =
dc_receive_imf(context, &body, folder.as_ref(), server_uid, is_seen)
dc_receive_imf(context, &body, folder.as_ref(), server_uid, flags as u32)
{
warn!(
context,
@@ -774,20 +731,9 @@ impl Imap {
);
}
}
} else {
warn!(
context,
"Message #{} does not exist in folder \"{}\".",
server_uid,
folder.as_ref()
);
}
Ok(())
}
pub fn can_move(&self) -> bool {
task::block_on(async move { self.config.read().await.can_move })
1
}
pub fn mv(
@@ -819,73 +765,51 @@ impl Imap {
let set = format!("{}", uid);
let display_folder_id = format!("{}/{}", folder, uid);
if self.can_move() {
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_mv(&set, &dest_folder).await {
Ok(_) => {
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} moved to {}",
display_folder_id, dest_folder
))
);
return ImapActionResult::Success;
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
folder,
uid,
dest_folder,
err
);
}
}
} else {
unreachable!();
};
} else {
info!(
context,
"Server does not support MOVE, fallback to COPY/DELETE {}/{} to {}",
folder,
uid,
dest_folder
);
}
if let Some(ref mut session) = &mut *self.session.lock().await {
if let Err(err) = session.uid_copy(&set, &dest_folder).await {
warn!(context, "Could not copy message: {}", err);
return ImapActionResult::Failed;
match session.uid_mv(&set, &dest_folder).await {
Ok(_) => {
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} moved to {}",
display_folder_id, dest_folder
))
);
return ImapActionResult::Success;
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
folder,
uid,
dest_folder,
err
);
}
}
} else {
unreachable!();
}
};
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete FAILED)",
display_folder_id, dest_folder
))
);
ImapActionResult::Failed
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_copy(&set, &dest_folder).await {
Ok(_) => {
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
ImapActionResult::Failed
} else {
self.config.write().await.selected_folder_needs_expunge = true;
ImapActionResult::Success
}
}
Err(err) => {
warn!(context, "Could not copy message: {}", err);
ImapActionResult::Failed
}
}
} else {
self.config.write().await.selected_folder_needs_expunge = true;
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete successfull)",
display_folder_id, dest_folder
))
);
ImapActionResult::Success
unreachable!();
}
})
}
@@ -991,6 +915,7 @@ impl Imap {
})
}
// only returns 0 on connection problems; we should try later again in this case *
pub fn delete_msg(
&self,
context: &Context,
@@ -1010,11 +935,9 @@ impl Imap {
// double-check that we are deleting the correct message-id
// this comes at the expense of another imap query
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_fetch(set, DELETE_CHECK_FLAGS).await {
match session.uid_fetch(set, PREFETCH_FLAGS).await {
Ok(msgs) => {
let fetch = if let Some(fetch) = msgs.first() {
fetch
} else {
if msgs.is_empty() {
warn!(
context,
"Cannot delete on IMAP, {}: imap entry gone '{}'",
@@ -1022,11 +945,9 @@ impl Imap {
message_id,
);
return ImapActionResult::Failed;
};
let remote_message_id = get_fetch_headers(fetch)
.and_then(|headers| prefetch_get_message_id(&headers))
.unwrap_or_default();
}
let remote_message_id =
prefetch_get_message_id(msgs.first().unwrap()).unwrap_or_default();
if remote_message_id != message_id {
warn!(
@@ -1178,6 +1099,7 @@ impl Imap {
}
async fn list_folders(&self, session: &mut Session, context: &Context) -> Option<Vec<Name>> {
// TODO: use xlist when available
match session.list(Some(""), Some("*")).await {
Ok(list) => {
if list.is_empty() {
@@ -1284,7 +1206,7 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
message::rfc724_mid_exists(context, &rfc724_mid)
{
if old_server_folder.is_empty() && old_server_uid == 0 {
info!(context, "[move] detected bcc-self {}", rfc724_mid,);
info!(context, "[move] detected bbc-self {}", rfc724_mid,);
context.do_heuristics_moves(server_folder.as_ref(), msg_id);
job_add(
context,
@@ -1306,70 +1228,46 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
}
}
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
let header_bytes = match prefetch_msg.header() {
Some(header_bytes) => header_bytes,
None => return Ok(Vec::new()),
};
let (headers, _) = mailparse::parse_headers(header_bytes)?;
Ok(headers)
}
fn parse_message_id(message_id: &[u8]) -> crate::error::Result<String> {
let value = std::str::from_utf8(message_id)?;
let addrs = mailparse::addrparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId)? {
Ok(crate::mimeparser::parse_message_id(&message_id)?)
} else {
Err(Error::Other("prefetch: No message ID found".to_string()))
}
}
fn prefetch_is_reply_to_chat_message(
context: &Context,
headers: &[mailparse::MailHeader],
) -> Result<bool> {
if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo)? {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
}
if let Some(info) = addrs.extract_single_info() {
return Ok(info.addr);
}
if let Some(value) = headers.get_header_value(HeaderDef::References)? {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
}
bail!("could not parse message_id: {}", value);
}
fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
if prefetch_msg.envelope().is_none() {
return Err(Error::Other(
"prefectch: message has no envelope".to_string(),
));
}
Ok(false)
let message_id = prefetch_msg.envelope().unwrap().message_id;
if message_id.is_none() {
return Err(Error::Other("prefetch: No message ID found".to_string()));
}
parse_message_id(&message_id.unwrap()).map_err(Into::into)
}
fn prefetch_should_download(
context: &Context,
headers: &[mailparse::MailHeader],
show_emails: ShowEmails,
) -> Result<bool> {
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion)?.is_some();
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers)?;
#[cfg(test)]
mod tests {
use super::*;
// Autocrypt Setup Message should be shown even if it is from non-chat client.
let is_autocrypt_setup_message = headers
.get_header_value(HeaderDef::AutocryptSetupMessage)?
.is_some();
let from_field = headers
.get_header_value(HeaderDef::From_)?
.unwrap_or_default();
let (_contact_id, blocked_contact, origin) = from_field_to_contact_id(context, &from_field)?;
let accepted_contact = origin.is_known();
let show = is_autocrypt_setup_message
|| match show_emails {
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
ShowEmails::AcceptedContacts => {
is_chat_message || is_reply_to_chat_message || accepted_contact
}
ShowEmails::All => true,
};
let show = show && !blocked_contact;
Ok(show)
#[test]
fn test_parse_message_id() {
assert_eq!(
parse_message_id(b"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
assert_eq!(
parse_message_id(b"<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
}
}

View File

@@ -1,18 +1,122 @@
use async_imap::{
error::Result as ImapResult,
error::{Error as ImapError, Result as ImapResult},
extensions::idle::Handle as ImapIdleHandle,
types::{Capabilities, Fetch, Mailbox, Name},
Session as ImapSession,
Client as ImapClient, Session as ImapSession,
};
use async_native_tls::TlsStream;
use async_std::net::TcpStream;
use async_std::net::{self, TcpStream};
use async_std::prelude::*;
use crate::login_param::{dc_build_tls, CertificateChecks};
#[derive(Debug)]
pub(crate) enum Client {
Secure(ImapClient<TlsStream<TcpStream>>),
Insecure(ImapClient<TcpStream>),
}
#[derive(Debug)]
pub(crate) enum Session {
Secure(ImapSession<TlsStream<TcpStream>>),
Insecure(ImapSession<TcpStream>),
}
#[derive(Debug)]
pub(crate) enum IdleHandle {
Secure(ImapIdleHandle<TlsStream<TcpStream>>),
Insecure(ImapIdleHandle<TcpStream>),
}
impl Client {
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(certificate_checks);
let tls_stream = tls.connect(domain.as_ref(), stream).await?;
let mut client = ImapClient::new(tls_stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Secure(client))
}
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let mut client = ImapClient::new(stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Insecure(client))
}
pub async fn secure<S: AsRef<str>>(
self,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Client> {
match self {
Client::Insecure(client) => {
let tls = dc_build_tls(certificate_checks);
let client_sec = client.secure(domain, tls).await?;
Ok(Client::Secure(client_sec))
}
// Nothing to do
Client::Secure(_) => Ok(self),
}
}
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
self,
auth_type: S,
authenticator: &A,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
}
}
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
self,
username: U,
password: P,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
}
}
}
impl Session {
pub async fn capabilities(&mut self) -> ImapResult<Capabilities> {
let res = match self {
@@ -123,6 +227,19 @@ impl Session {
Ok(res)
}
pub fn idle(self) -> IdleHandle {
match self {
Session::Secure(i) => {
let h = i.idle();
IdleHandle::Secure(h)
}
Session::Insecure(i) => {
let h = i.idle();
IdleHandle::Insecure(h)
}
}
}
pub async fn uid_store<S1, S2>(&mut self, uid_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
where
S1: AsRef<str>,

View File

@@ -17,7 +17,7 @@ use crate::e2ee;
use crate::error::*;
use crate::events::Event;
use crate::job::*;
use crate::key::{self, Key};
use crate::key::*;
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::*;
@@ -288,6 +288,7 @@ fn set_self_key(
.and_then(|(k, h)| k.split_key().map(|pub_key| (k, pub_key, h)));
ensure!(keys.is_some(), "Not a valid private key");
let (private_key, public_key, header) = keys.unwrap();
let preferencrypt = header.get("Autocrypt-Prefer-Encrypt");
match preferencrypt.map(|s| s.as_str()) {
@@ -312,26 +313,34 @@ fn set_self_key(
let self_addr = context.get_config(Config::ConfiguredAddr);
ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
let (public, secret) = match (public_key, private_key) {
(Key::Public(p), Key::Secret(s)) => (p, s),
_ => bail!("wrong keys unpacked"),
};
let keypair = pgp::KeyPair {
addr,
public,
secret,
};
key::store_self_keypair(
// XXX maybe better make dc_key_save_self_keypair delete things
sql::execute(
context,
&keypair,
if set_default {
key::KeyPairUse::Default
} else {
key::KeyPairUse::ReadOnly
},
&context.sql,
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
params![public_key.to_bytes(), private_key.to_bytes()],
)?;
if set_default {
sql::execute(
context,
&context.sql,
"UPDATE keypairs SET is_default=0;",
params![],
)?;
}
if !dc_key_save_self_keypair(
context,
&public_key,
&private_key,
self_addr.unwrap_or_default(),
set_default,
&context.sql,
) {
bail!("Cannot save keypair, internal key-state possibly corrupted now!");
}
Ok(())
}
@@ -736,6 +745,7 @@ fn export_key_to_asc_file(
#[cfg(test)]
mod tests {
use super::*;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::test_utils::*;
use ::pgp::armor::BlockType;
@@ -790,7 +800,8 @@ mod tests {
#[test]
fn test_export_key_to_asc_file() {
let context = dummy_context();
let key = Key::from(alice_keypair().public);
let base64 = include_str!("../test-data/key/public.asc");
let key = Key::from_base64(base64, KeyType::Public).unwrap();
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key).is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();

View File

@@ -193,31 +193,9 @@ impl Job {
Err(crate::smtp::send::Error::SendError(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {}", err);
self.pending_error = Some(err.to_string());
let res = match err {
async_smtp::smtp::error::Error::Permanent(_) => {
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
}
async_smtp::smtp::error::Error::Transient(_) => {
// We got a transient 4xx response from SMTP server.
// Give some time until the server-side error maybe goes away.
Status::RetryLater
}
_ => {
if smtp.has_maybe_stale_connection() {
info!(context, "stale connection? immediately reconnecting");
Status::RetryNow
} else {
Status::RetryLater
}
}
};
// this clears last_success info
smtp.disconnect();
res
self.pending_error = Some(err.to_string());
Status::RetryLater
}
Err(crate::smtp::send::Error::EnvelopeError(err)) => {
// Local error, job is invalid, do not retry.
@@ -391,7 +369,7 @@ impl Job {
}
self.smtp_send(context, recipients, body, self.job_id, || {
// Remove additional SendMdn jobs we have aggregated into this one.
// Remove additional SendMdn jobs we have aggretated into this one.
job_kill_ids(context, &additional_job_ids)?;
Ok(())
})
@@ -575,7 +553,7 @@ pub fn job_kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
)
}
pub fn perform_inbox_fetch(context: &Context) {
pub(crate) fn perform_inbox_fetch(context: &Context) {
let use_network = context.get_config_bool(Config::InboxWatch);
task::block_on(
@@ -587,7 +565,7 @@ pub fn perform_inbox_fetch(context: &Context) {
);
}
pub fn perform_mvbox_fetch(context: &Context) {
pub(crate) fn perform_mvbox_fetch(context: &Context) {
let use_network = context.get_config_bool(Config::MvboxWatch);
task::block_on(
@@ -599,7 +577,7 @@ pub fn perform_mvbox_fetch(context: &Context) {
);
}
pub fn perform_sentbox_fetch(context: &Context) {
pub(crate) fn perform_sentbox_fetch(context: &Context) {
let use_network = context.get_config_bool(Config::SentboxWatch);
task::block_on(
@@ -611,7 +589,7 @@ pub fn perform_sentbox_fetch(context: &Context) {
);
}
pub fn perform_inbox_idle(context: &Context) {
pub(crate) fn perform_inbox_idle(context: &Context) {
if *context.perform_inbox_jobs_needed.clone().read().unwrap() {
info!(
context,
@@ -628,7 +606,7 @@ pub fn perform_inbox_idle(context: &Context) {
.idle(context, use_network);
}
pub fn perform_mvbox_idle(context: &Context) {
pub(crate) fn perform_mvbox_idle(context: &Context) {
let use_network = context.get_config_bool(Config::MvboxWatch);
context
@@ -638,7 +616,7 @@ pub fn perform_mvbox_idle(context: &Context) {
.idle(context, use_network);
}
pub fn perform_sentbox_idle(context: &Context) {
pub(crate) fn perform_sentbox_idle(context: &Context) {
let use_network = context.get_config_bool(Config::SentboxWatch);
context
@@ -648,7 +626,7 @@ pub fn perform_sentbox_idle(context: &Context) {
.idle(context, use_network);
}
pub fn interrupt_inbox_idle(context: &Context) {
pub(crate) fn interrupt_inbox_idle(context: &Context) {
info!(context, "interrupt_inbox_idle called");
// we do not block on trying to obtain the thread lock
// because we don't know in which state the thread is.
@@ -665,11 +643,11 @@ pub fn interrupt_inbox_idle(context: &Context) {
}
}
pub fn interrupt_mvbox_idle(context: &Context) {
pub(crate) fn interrupt_mvbox_idle(context: &Context) {
context.mvbox_thread.read().unwrap().interrupt_idle(context);
}
pub fn interrupt_sentbox_idle(context: &Context) {
pub(crate) fn interrupt_sentbox_idle(context: &Context) {
context
.sentbox_thread
.read()
@@ -677,7 +655,7 @@ pub fn interrupt_sentbox_idle(context: &Context) {
.interrupt_idle(context);
}
pub fn perform_smtp_jobs(context: &Context) {
pub(crate) fn perform_smtp_jobs(context: &Context) {
let probe_smtp_network = {
let &(ref lock, _) = &*context.smtp_state.clone();
let mut state = lock.lock().unwrap();
@@ -706,7 +684,7 @@ pub fn perform_smtp_jobs(context: &Context) {
}
}
pub fn perform_smtp_idle(context: &Context) {
pub(crate) fn perform_smtp_idle(context: &Context) {
info!(context, "SMTP-idle started...",);
{
let &(ref lock, ref cvar) = &*context.smtp_state.clone();
@@ -814,32 +792,7 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
};
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar)?;
let mut recipients = mimefactory.recipients();
let from = context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
let lowercase_from = from.to_lowercase();
if context.get_config_bool(Config::BccSelf)
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
recipients.push(from);
}
if recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
set_delivered(context, msg_id);
return Ok(());
}
let rendered_msg = mimefactory.render().map_err(|err| {
let mut rendered_msg = mimefactory.render().map_err(|err| {
message::set_msg_failed(context, msg_id, Some(err.to_string()));
err
})?;
@@ -858,6 +811,26 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
);
}
let lowercase_from = rendered_msg.from.to_lowercase();
if context.get_config_bool(Config::BccSelf)
&& !rendered_msg
.recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
rendered_msg.recipients.push(rendered_msg.from.clone());
}
if rendered_msg.recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
set_delivered(context, msg_id);
return Ok(());
}
if rendered_msg.is_gossiped {
chat::set_gossiped_timestamp(context, msg.chat_id, time())?;
}
@@ -886,18 +859,12 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
msg.save_param_to_disk(context);
}
add_smtp_job(
context,
Action::SendMsgToSmtp,
msg.id,
recipients,
&rendered_msg,
)?;
add_smtp_job(context, Action::SendMsgToSmtp, msg.id, &rendered_msg)?;
Ok(())
}
pub fn perform_inbox_jobs(context: &Context) {
pub(crate) fn perform_inbox_jobs(context: &Context) {
info!(context, "dc_perform_inbox_jobs starting.",);
let probe_imap_network = *context.probe_imap_network.clone().read().unwrap();
@@ -908,22 +875,14 @@ pub fn perform_inbox_jobs(context: &Context) {
info!(context, "dc_perform_inbox_jobs ended.",);
}
pub fn perform_mvbox_jobs(context: &Context) {
info!(context, "dc_perform_mbox_jobs EMPTY (for now).",);
}
pub fn perform_sentbox_jobs(context: &Context) {
info!(context, "dc_perform_sentbox_jobs EMPTY (for now).",);
}
fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
while let Some(mut job) = load_next_job(context, thread, probe_network) {
info!(context, "{}-job {} started...", thread, job);
// some configuration jobs are "exclusive":
// - they are always executed in the imap-thread and the smtp-thread is suspended during execution
// - they may change the database handle; we do not keep old pointers therefore
// - they can be re-executed one time AT_ONCE, but they are not saved in the database for later execution
// - they may change the database handle change the database handle; we do not keep old pointers therefore
// - they can be re-executed one time AT_ONCE, but they are not save in the database for later execution
if Action::ConfigureImap == job.action || Action::ImexImap == job.action {
job_kill_action(context, job.action);
context
@@ -941,10 +900,52 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
suspend_smtp_thread(context, true);
}
let try_res = match perform_job_action(context, &mut job, thread, 0) {
Status::RetryNow => perform_job_action(context, &mut job, thread, 1),
x => x,
};
let try_res = (0..2)
.map(|tries| {
info!(
context,
"{} performs immediate try {} of job {}", thread, tries, job
);
let try_res = match job.action {
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
Action::SendMsgToSmtp => job.SendMsgToSmtp(context),
Action::EmptyServer => job.EmptyServer(context),
Action::DeleteMsgOnImap => job.DeleteMsgOnImap(context),
Action::MarkseenMsgOnImap => job.MarkseenMsgOnImap(context),
Action::MarkseenMdnOnImap => job.MarkseenMdnOnImap(context),
Action::MoveMsg => job.MoveMsg(context),
Action::SendMdn => job.SendMdn(context),
Action::ConfigureImap => JobConfigureImap(context),
Action::ImexImap => match JobImexImap(context, &job) {
Ok(()) => Status::Finished(Ok(())),
Err(err) => {
error!(context, "{}", err);
Status::Finished(Err(err))
}
},
Action::MaybeSendLocations => location::JobMaybeSendLocations(context, &job),
Action::MaybeSendLocationsEnded => {
location::JobMaybeSendLocationsEnded(context, &mut job)
}
Action::Housekeeping => {
sql::housekeeping(context);
Status::Finished(Ok(()))
}
};
info!(
context,
"{} finished immediate try {} of job {}", thread, tries, job
);
try_res
})
.find(|try_res| match try_res {
Status::RetryNow => false,
_ => true,
})
.unwrap_or(Status::RetryNow);
if Action::ConfigureImap == job.action || Action::ImexImap == job.action {
context
@@ -1035,45 +1036,6 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
}
}
fn perform_job_action(context: &Context, mut job: &mut Job, thread: Thread, tries: u32) -> Status {
info!(
context,
"{} begin immediate try {} of job {}", thread, tries, job
);
let try_res = match job.action {
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
Action::SendMsgToSmtp => job.SendMsgToSmtp(context),
Action::EmptyServer => job.EmptyServer(context),
Action::DeleteMsgOnImap => job.DeleteMsgOnImap(context),
Action::MarkseenMsgOnImap => job.MarkseenMsgOnImap(context),
Action::MarkseenMdnOnImap => job.MarkseenMdnOnImap(context),
Action::MoveMsg => job.MoveMsg(context),
Action::SendMdn => job.SendMdn(context),
Action::ConfigureImap => JobConfigureImap(context),
Action::ImexImap => match JobImexImap(context, &job) {
Ok(()) => Status::Finished(Ok(())),
Err(err) => {
error!(context, "{}", err);
Status::Finished(Err(err))
}
},
Action::MaybeSendLocations => location::JobMaybeSendLocations(context, &job),
Action::MaybeSendLocationsEnded => location::JobMaybeSendLocationsEnded(context, &mut job),
Action::Housekeeping => {
sql::housekeeping(context);
Status::Finished(Ok(()))
}
};
info!(
context,
"{} finished immediate try {} of job {}", thread, tries, job
);
try_res
}
fn get_backoff_time_offset(tries: u32) -> i64 {
let n = 2_i32.pow(tries - 1) * 60;
let mut rng = thread_rng();
@@ -1110,15 +1072,17 @@ fn add_smtp_job(
context: &Context,
action: Action,
msg_id: MsgId,
recipients: Vec<String>,
rendered_msg: &RenderedEmail,
) -> Result<()> {
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
ensure!(
!rendered_msg.recipients.is_empty(),
"no recipients for smtp job set"
);
let mut param = Params::new();
let bytes = &rendered_msg.message;
let blob = BlobObject::create(context, &rendered_msg.rfc724_mid, bytes)?;
let recipients = recipients.join("\x1e");
let recipients = rendered_msg.recipients.join("\x1e");
param.set(Param::File, blob.as_name());
param.set(Param::Recipients, &recipients);
@@ -1165,7 +1129,7 @@ pub fn job_add(
}
}
pub fn interrupt_smtp_idle(context: &Context) {
pub(crate) fn interrupt_smtp_idle(context: &Context) {
info!(context, "Interrupting SMTP-idle...",);
let &(ref lock, ref cvar) = &*context.smtp_state.clone();
@@ -1181,7 +1145,7 @@ pub fn interrupt_smtp_idle(context: &Context) {
///
/// Load jobs for this "[Thread]", i.e. either load SMTP jobs or load
/// IMAP jobs. The `probe_network` parameter decides how to query
/// jobs, this is tricky and probably wrong currently. Look at the
/// jobs, this is tricky and probably wrong currently. Look at the
/// SQL queries for details.
fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Option<Job> {
let query = if !probe_network {

View File

@@ -143,7 +143,7 @@ impl JobThread {
if state.jobs_needed {
info!(
context,
"{}-IDLE will not be started as it was interrupted while not idling.",
"{}-IDLE will not be started as it was interrupted while not ideling.",
self.name,
);
state.jobs_needed = false;

View File

@@ -4,84 +4,14 @@ use std::collections::BTreeMap;
use std::io::Cursor;
use std::path::Path;
use pgp::composed::Deserializable;
use pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::sql::Sql;
// Re-export key types
pub use crate::pgp::KeyPair;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
/// Error type for deltachat key handling.
#[derive(Fail, Debug)]
pub enum Error {
#[fail(display = "Could not decode base64")]
Base64Decode(#[cause] base64::DecodeError, failure::Backtrace),
#[fail(display = "rPGP error: {}", _0)]
PgpError(#[cause] pgp::errors::Error, failure::Backtrace),
}
impl From<base64::DecodeError> for Error {
fn from(err: base64::DecodeError) -> Error {
Error::Base64Decode(err, failure::Backtrace::new())
}
}
impl From<pgp::errors::Error> for Error {
fn from(err: pgp::errors::Error) -> Error {
Error::PgpError(err, failure::Backtrace::new())
}
}
pub type Result<T> = std::result::Result<T, Error>;
/// Convenience trait for working with keys.
///
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub trait DcKey: Serialize + Deserializable {
type KeyType: Serialize + Deserializable;
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self::KeyType> {
Ok(<Self::KeyType as Deserializable>::from_bytes(Cursor::new(
bytes,
))?)
}
/// Create a key from a base64 string.
fn from_base64(data: &str) -> Result<Self::KeyType> {
// strip newlines and other whitespace
let cleaned: String = data.trim().split_whitespace().collect();
let bytes = base64::decode(cleaned.as_bytes())?;
Self::from_slice(&bytes)
}
/// Serialise the key to a base64 string.
fn to_base64(&self) -> String {
// Not using Serialize::to_bytes() to make clear *why* it is
// safe to ignore this error.
// Because we write to a Vec<u8> the io::Write impls never
// fail and we can hide this error.
let mut buf = Vec::new();
self.to_writer(&mut buf).unwrap();
base64::encode(&buf)
}
}
impl DcKey for SignedPublicKey {
type KeyType = SignedPublicKey;
}
impl DcKey for SignedSecretKey {
type KeyType = SignedSecretKey;
}
use crate::sql::{self, Sql};
/// Cryptographic key
#[derive(Debug, PartialEq, Eq, Clone)]
@@ -105,7 +35,7 @@ impl From<SignedSecretKey> for Key {
impl std::convert::TryFrom<Key> for SignedSecretKey {
type Error = ();
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
fn try_from(value: Key) -> Result<Self, Self::Error> {
match value {
Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key),
@@ -116,7 +46,7 @@ impl std::convert::TryFrom<Key> for SignedSecretKey {
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
type Error = ();
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
fn try_from(value: &'a Key) -> Result<Self, Self::Error> {
match value {
Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key),
@@ -127,7 +57,7 @@ impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
impl std::convert::TryFrom<Key> for SignedPublicKey {
type Error = ();
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
fn try_from(value: Key) -> Result<Self, Self::Error> {
match value {
Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()),
@@ -138,7 +68,7 @@ impl std::convert::TryFrom<Key> for SignedPublicKey {
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedPublicKey {
type Error = ();
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
fn try_from(value: &'a Key) -> Result<Self, Self::Error> {
match value {
Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()),
@@ -162,7 +92,7 @@ impl Key {
if bytes.is_empty() {
return None;
}
let res: std::result::Result<Key, _> = match key_type {
let res: Result<Key, _> = match key_type {
KeyType::Public => SignedPublicKey::from_bytes(Cursor::new(bytes)).map(Into::into),
KeyType::Private => SignedSecretKey::from_bytes(Cursor::new(bytes)).map(Into::into),
};
@@ -181,7 +111,7 @@ impl Key {
key_type: KeyType,
) -> Option<(Self, BTreeMap<String, String>)> {
let bytes = data.as_bytes();
let res: std::result::Result<(Key, _), _> = match key_type {
let res: Result<(Key, _), _> = match key_type {
KeyType::Public => SignedPublicKey::from_armor_single(Cursor::new(bytes))
.map(|(k, h)| (Into::into(k), h)),
KeyType::Private => SignedSecretKey::from_armor_single(Cursor::new(bytes))
@@ -197,6 +127,15 @@ impl Key {
}
}
pub fn from_base64(encoded_data: &str, key_type: KeyType) -> Option<Self> {
// strip newlines and other whitespace
let cleaned: String = encoded_data.trim().split_whitespace().collect();
let bytes = cleaned.as_bytes();
base64::decode(bytes)
.ok()
.and_then(|decoded| Self::from_slice(&decoded, key_type))
}
pub fn from_self_public(
context: &Context,
self_addr: impl AsRef<str>,
@@ -303,97 +242,20 @@ impl Key {
}
}
/// Use of a [KeyPair] for encryption or decryption.
///
/// This is used by [store_self_keypair] to know what kind of key is
/// being saved.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum KeyPairUse {
/// The default key used to encrypt new messages.
Default,
/// Only used to decrypt existing message.
ReadOnly,
}
/// Error saving a keypair to the database.
#[derive(Fail, Debug)]
#[fail(display = "SaveKeyError: {}", message)]
pub struct SaveKeyError {
message: String,
#[cause]
cause: failure::Error,
backtrace: failure::Backtrace,
}
impl SaveKeyError {
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
Self {
message: message.into(),
cause: cause.into(),
backtrace: failure::Backtrace::new(),
}
}
}
/// Store the keypair as an owned keypair for addr in the database.
///
/// This will save the keypair as keys for the given address. The
/// "self" here refers to the fact that this DC instance owns the
/// keypair. Usually `addr` will be [Config::ConfiguredAddr].
///
/// If either the public or private keys are already present in the
/// database, this entry will be removed first regardless of the
/// address associated with it. Practically this means saving the
/// same key again overwrites it.
///
/// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr
pub fn store_self_keypair(
pub fn dc_key_save_self_keypair(
context: &Context,
keypair: &KeyPair,
default: KeyPairUse,
) -> std::result::Result<(), SaveKeyError> {
// Everything should really be one transaction, more refactoring
// is needed for that.
let public_key = keypair
.public
.to_bytes()
.map_err(|err| SaveKeyError::new("failed to serialise public key", err))?;
let secret_key = keypair
.secret
.to_bytes()
.map_err(|err| SaveKeyError::new("failed to serialise secret key", err))?;
context
.sql
.execute(
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
params![public_key, secret_key],
)
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default {
context
.sql
.execute("UPDATE keypairs SET is_default=0;", params![])
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
}
let is_default = match default {
KeyPairUse::Default => true,
KeyPairUse::ReadOnly => false,
};
context
.sql
.execute(
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
VALUES (?,?,?,?,?);",
params![
keypair.addr.to_string(),
is_default as i32,
public_key,
secret_key,
time()
],
)
.map(|_| ())
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))
public_key: &Key,
private_key: &Key,
addr: impl AsRef<str>,
is_default: bool,
sql: &Sql,
) -> bool {
sql::execute(
context,
sql,
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);",
params![addr.as_ref(), is_default as i32, public_key.to_bytes(), private_key.to_bytes(), time()],
).is_ok()
}
/// Make a fingerprint human-readable, in hex format.
@@ -425,14 +287,6 @@ pub fn dc_normalize_fingerprint(fp: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use std::convert::TryFrom;
use lazy_static::lazy_static;
lazy_static! {
static ref KEYPAIR: KeyPair = alice_keypair();
}
#[test]
fn test_normalize_fingerprint() {
@@ -519,9 +373,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
#[test]
#[ignore] // is too expensive
fn test_from_slice_roundtrip() {
let public_key = Key::from(KEYPAIR.public.clone());
let private_key = Key::from(KEYPAIR.secret.clone());
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap();
let binary = public_key.to_bytes();
let public_key2 = Key::from_slice(&binary, KeyType::Public).expect("invalid public key");
@@ -554,9 +408,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
#[test]
#[ignore] // is too expensive
fn test_ascii_roundtrip() {
let public_key = Key::from(KEYPAIR.public.clone());
let private_key = Key::from(KEYPAIR.secret.clone());
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap();
let s = public_key.to_armored_string(None).unwrap();
let (public_key2, _) =
@@ -569,51 +423,4 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
Key::from_armored_string(&s, KeyType::Private).expect("invalid private key");
assert_eq!(private_key, private_key2);
}
#[test]
fn test_split_key() {
let private_key = Key::from(KEYPAIR.secret.clone());
let public_wrapped = private_key.split_key().unwrap();
let public = SignedPublicKey::try_from(public_wrapped).unwrap();
assert_eq!(public.primary_key, KEYPAIR.public.primary_key);
}
#[test]
fn test_save_self_key_twice() {
// Saving the same key twice should result in only one row in
// the keypairs table.
let t = dummy_context();
let nrows = || {
t.ctx
.sql
.query_get_value::<_, u32>(&t.ctx, "SELECT COUNT(*) FROM keypairs;", params![])
.unwrap()
};
assert_eq!(nrows(), 0);
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
assert_eq!(nrows(), 1);
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
assert_eq!(nrows(), 1);
}
// Convenient way to create a new key if you need one, run with
// `cargo test key::tests::gen_key`.
// #[test]
// fn gen_key() {
// let name = "fiona";
// let keypair = crate::pgp::create_keypair(
// EmailAddress::new(&format!("{}@example.net", name)).unwrap(),
// )
// .unwrap();
// std::fs::write(
// format!("test-data/key/{}-public.asc", name),
// keypair.public.to_base64(),
// )
// .unwrap();
// std::fs::write(
// format!("test-data/key/{}-secret.asc", name),
// keypair.secret.to_base64(),
// )
// .unwrap();
// }
}

View File

@@ -1,8 +1,8 @@
use std::borrow::Cow;
use crate::constants::KeyType;
use crate::constants::*;
use crate::context::Context;
use crate::key::Key;
use crate::key::*;
use crate::sql::Sql;
#[derive(Default, Clone, Debug)]

View File

@@ -1,4 +1,3 @@
#![forbid(unsafe_code)]
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
#![allow(clippy::match_bool)]
@@ -27,22 +26,23 @@ pub(crate) mod events;
pub use events::*;
mod aheader;
mod blob;
pub mod blob;
pub mod chat;
pub mod chatlist;
pub mod config;
mod configure;
pub mod configure;
pub mod constants;
pub mod contact;
pub mod context;
mod e2ee;
mod imap;
mod imap_client;
pub mod imex;
#[macro_use]
pub mod job;
mod job_thread;
pub mod key;
mod keyring;
pub mod keyring;
pub mod location;
mod login_param;
pub mod lot;
@@ -53,7 +53,6 @@ pub mod oauth2;
mod param;
pub mod peerstate;
pub mod pgp;
pub mod provider;
pub mod qr;
pub mod securejoin;
mod simplify;

View File

@@ -64,10 +64,11 @@ impl Kml {
Default::default()
}
pub fn parse(context: &Context, to_parse: &[u8]) -> Result<Self, Error> {
ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large");
pub fn parse(context: &Context, content: &[u8]) -> Result<Self, Error> {
ensure!(content.len() <= 1024 * 1024, "kml-file is too large");
let mut reader = quick_xml::Reader::from_reader(to_parse);
let to_parse = String::from_utf8_lossy(content);
let mut reader = quick_xml::Reader::from_str(&to_parse);
reader.trim_text(true);
let mut kml = Kml::new();
@@ -364,7 +365,6 @@ fn is_marker(txt: &str) -> bool {
txt.len() == 1 && !txt.starts_with(' ')
}
/// Deletes all locations from the database.
pub fn delete_all(context: &Context) -> Result<(), Error> {
sql::execute(context, &context.sql, "DELETE FROM locations;", params![])?;
context.call_cb(Event::LocationChanged(None));
@@ -548,7 +548,7 @@ pub fn save(
}
#[allow(non_snake_case)]
pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
pub fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
let now = time();
let mut continue_streaming = false;
info!(
@@ -639,7 +639,7 @@ pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Statu
}
#[allow(non_snake_case)]
pub(crate) fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status {
pub fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status {
// this function is called when location-streaming _might_ have ended for a chat.
// the function checks, if location-streaming is really ended;
// if so, a device-message is added if not yet done.

View File

@@ -86,9 +86,6 @@ pub enum LotState {
/// test1=formatted fingerprint
QrFprWithoutAddr = 230,
/// text1=domain
QrAccount = 250,
/// id=contact
QrAddr = 320,

View File

@@ -4,7 +4,6 @@ use std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use failure::Fail;
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatId};
use crate::constants::*;
@@ -30,9 +29,7 @@ const SUMMARY_CHARACTERS: usize = 160;
/// Some message IDs are reserved to identify special message types.
/// This type can represent both the special as well as normal
/// messages.
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
pub struct MsgId(u32);
impl MsgId {
@@ -148,20 +145,9 @@ impl rusqlite::types::FromSql for MsgId {
#[fail(display = "Invalid Message ID.")]
pub struct InvalidMsgId;
#[derive(
Debug,
Copy,
Clone,
PartialEq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[derive(Debug, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub(crate) enum MessengerMessage {
pub enum MessengerMessage {
No = 0,
Yes = 1,
@@ -182,7 +168,7 @@ impl Default for MessengerMessage {
/// to check if a mail was sent, use dc_msg_is_sent()
/// approx. max. length returned by dc_msg_get_text()
/// approx. max. length returned by dc_get_msg_info()
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default)]
pub struct Message {
pub(crate) id: MsgId,
pub(crate) from_id: u32,
@@ -217,7 +203,8 @@ impl Message {
pub fn load_from_db(context: &Context, id: MsgId) -> Result<Message, Error> {
ensure!(
!id.is_special(),
"Can not load special message IDs from DB."
"Can not load special message IDs from DB. ({:?})",
id
);
context
.sql
@@ -621,19 +608,7 @@ impl Message {
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
ToSql,
FromSql,
Serialize,
Deserialize,
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[repr(i32)]
pub enum MessageState {
Undefined = 0,

View File

@@ -60,6 +60,9 @@ pub struct RenderedEmail {
pub is_gossiped: bool,
pub last_added_location_id: u32,
pub from: String,
pub recipients: Vec<String>,
/// Message ID (Message in the sense of Email)
pub rfc724_mid: String,
}
@@ -68,7 +71,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
pub fn from_msg(
context: &'a Context,
msg: &'b Message,
attach_selfavatar: bool,
add_selfavatar: bool,
) -> Result<MimeFactory<'a, 'b>, Error> {
let chat = Chat::load_from_db(context, msg.chat_id)?;
@@ -156,7 +159,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
references,
req_mdn,
last_added_location_id: 0,
attach_selfavatar,
attach_selfavatar: add_selfavatar,
context,
};
Ok(factory)
@@ -346,13 +349,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
pub fn recipients(&self) -> Vec<String> {
self.recipients
.iter()
.map(|(_, addr)| addr.clone())
.collect()
}
pub fn render(mut self) -> Result<RenderedEmail, Error> {
// Headers that are encrypted
// - Chat-*, except Chat-Version
@@ -569,6 +565,8 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
};
let MimeFactory {
recipients,
from_addr,
last_added_location_id,
..
} = self;
@@ -579,6 +577,8 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
is_encrypted,
is_gossiped,
last_added_location_id,
recipients: recipients.into_iter().map(|(_, addr)| addr).collect(),
from: from_addr,
rfc724_mid,
})
}
@@ -638,6 +638,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let command = self.msg.param.get_cmd();
let mut placeholdertext = None;
let mut meta_part = None;
let mut add_compatibility_header = false;
if chat.typ == Chattype::VerifiedGroup {
protected_headers.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
@@ -680,6 +681,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
"vg-member-added".to_string(),
));
}
add_compatibility_header = true;
}
SystemMessage::GroupNameChanged => {
let value_to_add = self.msg.param.get(Param::Arg).unwrap_or_default();
@@ -700,6 +702,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
"0".to_string(),
));
}
add_compatibility_header = true;
}
_ => {}
}
@@ -767,7 +770,18 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image")?;
meta_part = Some(mail);
protected_headers.push(Header::new("Chat-Group-Avatar".into(), filename_as_sent));
protected_headers.push(Header::new(
"Chat-Group-Avatar".into(),
filename_as_sent.clone(),
));
// add the old group-image headers for versions <=0.973 resp. <=beta.15 (december 2019)
// image deletion is not supported in the compatibility layer.
// this can be removed some time after releasing 1.0,
// grep for #DeprecatedAvatar to get the place where compatibility parsing takes place.
if add_compatibility_header {
protected_headers.push(Header::new("Chat-Group-Image".into(), filename_as_sent));
}
}
if self.msg.viewtype == Viewtype::Sticker {
@@ -866,7 +880,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
if self.attach_selfavatar {
match context.get_config(Config::Selfavatar) {
Some(path) => match build_selfavatar_file(context, &path) {
Some(path) => match build_selfavatar_file(context, path) {
Ok((part, filename)) => {
parts.push(part);
protected_headers.push(Header::new("Chat-User-Avatar".into(), filename))
@@ -1061,7 +1075,7 @@ fn build_body_file(
Ok((mail, filename_to_send))
}
fn build_selfavatar_file(context: &Context, path: &str) -> Result<(PartBuilder, String), Error> {
fn build_selfavatar_file(context: &Context, path: String) -> Result<(PartBuilder, String), Error> {
let blob = BlobObject::from_path(context, path)?;
let filename_to_send = match blob.suffix() {
Some(suffix) => format!("avatar.{}", suffix),
@@ -1106,7 +1120,7 @@ fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
fn render_rfc724_mid(rfc724_mid: &str) -> String {
let rfc724_mid = rfc724_mid.trim().to_string();
if rfc724_mid.chars().nth(0).unwrap_or_default() == '<' {
if rfc724_mid.chars().next().unwrap_or_default() == '<' {
rfc724_mid
} else {
format!("<{}>", rfc724_mid)

View File

@@ -5,7 +5,6 @@ use lettre_email::mime::{self, Mime};
use mailparse::{DispositionType, MailAddr, MailHeaderMap};
use crate::aheader::Aheader;
use crate::bail;
use crate::blob::BlobObject;
use crate::config::Config;
use crate::constants::Viewtype;
@@ -16,7 +15,7 @@ use crate::dehtml::dehtml;
use crate::e2ee;
use crate::error::Result;
use crate::events::Event;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::headerdef::HeaderDef;
use crate::job::{job_add, Action};
use crate::location;
use crate::message;
@@ -25,6 +24,7 @@ use crate::peerstate::Peerstate;
use crate::securejoin::handle_degrade_event;
use crate::simplify::*;
use crate::stock::StockMessage;
use crate::{bail, ensure};
/// A parsed MIME message.
///
@@ -36,7 +36,8 @@ use crate::stock::StockMessage;
/// It is created by parsing the raw data of an actual MIME message
/// using the [MimeMessage::from_bytes] constructor.
#[derive(Debug)]
pub struct MimeMessage {
pub struct MimeMessage<'a> {
pub context: &'a Context,
pub parts: Vec<Part>,
header: HashMap<String, String>,
pub decrypting_failed: bool,
@@ -46,17 +47,28 @@ pub struct MimeMessage {
pub is_system_message: SystemMessage,
pub location_kml: Option<location::Kml>,
pub message_kml: Option<location::Kml>,
pub(crate) user_avatar: Option<AvatarAction>,
pub(crate) group_avatar: Option<AvatarAction>,
pub user_avatar: AvatarAction,
pub group_avatar: AvatarAction,
pub(crate) reports: Vec<Report>,
}
#[derive(Debug, PartialEq)]
pub(crate) enum AvatarAction {
pub enum AvatarAction {
None,
Delete,
Change(String),
}
impl AvatarAction {
pub fn is_change(&self) -> bool {
match self {
AvatarAction::None => false,
AvatarAction::Delete => false,
AvatarAction::Change(_) => true,
}
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[repr(i32)]
pub enum SystemMessage {
@@ -79,13 +91,13 @@ impl Default for SystemMessage {
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
impl MimeMessage {
pub fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
impl<'a> MimeMessage<'a> {
pub fn from_bytes(context: &'a Context, body: &[u8]) -> Result<Self> {
let mail = mailparse::parse_mail(body)?;
let message_time = mail
.headers
.get_header_value(HeaderDef::Date)?
.get_first_value("Date")?
.and_then(|v| mailparse::dateparse(&v).ok())
.unwrap_or_default();
@@ -148,21 +160,22 @@ impl MimeMessage {
signatures,
gossipped_addr,
is_forwarded: false,
context,
reports: Vec::new(),
is_system_message: SystemMessage::Unknown,
location_kml: None,
message_kml: None,
user_avatar: None,
group_avatar: None,
user_avatar: AvatarAction::None,
group_avatar: AvatarAction::None,
};
parser.parse_mime_recursive(context, &mail)?;
parser.parse_headers(context)?;
parser.parse_mime_recursive(&mail)?;
parser.parse_headers()?;
Ok(parser)
}
/// Parses system messages.
fn parse_system_message_headers(&mut self, context: &Context) -> Result<()> {
fn parse_system_message_headers(&mut self) -> Result<()> {
if self.get(HeaderDef::AutocryptSetupMessage).is_some() {
self.parts = self
.parts
@@ -177,7 +190,7 @@ impl MimeMessage {
if self.parts.len() == 1 {
self.is_system_message = SystemMessage::AutocryptSetupMessage;
} else {
warn!(context, "could not determine ASM mime-part");
warn!(self.context, "could not determine ASM mime-part");
}
} else if let Some(value) = self.get(HeaderDef::ChatContent) {
if value == "location-streaming-enabled" {
@@ -191,6 +204,10 @@ impl MimeMessage {
fn parse_avatar_headers(&mut self) {
if let Some(header_value) = self.get(HeaderDef::ChatGroupAvatar).cloned() {
self.group_avatar = self.avatar_action_from_header(header_value);
} else if let Some(header_value) = self.get(HeaderDef::ChatGroupImage).cloned() {
// parse the old group-image headers for versions <=0.973 resp. <=beta.15 (december 2019)
// grep for #DeprecatedAvatar to get the place where a compatibility header is generated.
self.group_avatar = self.avatar_action_from_header(header_value);
}
if let Some(header_value) = self.get(HeaderDef::ChatUserAvatar).cloned() {
@@ -267,8 +284,8 @@ impl MimeMessage {
}
}
fn parse_headers(&mut self, context: &Context) -> Result<()> {
self.parse_system_message_headers(context)?;
fn parse_headers(&mut self) -> Result<()> {
self.parse_system_message_headers()?;
self.parse_avatar_headers();
self.squash_attachment_parts();
@@ -313,9 +330,9 @@ impl MimeMessage {
// See if an MDN is requested from the other side
if !self.decrypting_failed && !self.parts.is_empty() {
if let Some(ref dn_to_addr) =
self.parse_first_addr(context, HeaderDef::ChatDispositionNotificationTo)
self.parse_first_addr(HeaderDef::ChatDispositionNotificationTo)
{
if let Some(ref from_addr) = self.parse_first_addr(context, HeaderDef::From_) {
if let Some(ref from_addr) = self.parse_first_addr(HeaderDef::From_) {
if compare_addrs(from_addr, dn_to_addr) {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
@@ -345,9 +362,9 @@ impl MimeMessage {
Ok(())
}
fn avatar_action_from_header(&mut self, header_value: String) -> Option<AvatarAction> {
fn avatar_action_from_header(&mut self, header_value: String) -> AvatarAction {
if header_value == "0" {
Some(AvatarAction::Delete)
return AvatarAction::Delete;
} else {
let mut i = 0;
while i != self.parts.len() {
@@ -355,7 +372,7 @@ impl MimeMessage {
if let Some(part_filename) = &part.org_filename {
if part_filename == &header_value {
if let Some(blob) = part.param.get(Param::File) {
let res = Some(AvatarAction::Change(blob.to_string()));
let res = AvatarAction::Change(blob.to_string());
self.parts.remove(i);
return res;
}
@@ -364,8 +381,8 @@ impl MimeMessage {
}
i += 1;
}
None
}
AvatarAction::None
}
pub fn was_encrypted(&self) -> bool {
@@ -387,38 +404,34 @@ impl MimeMessage {
}
pub fn get(&self, headerdef: HeaderDef) -> Option<&String> {
self.header.get(headerdef.get_headername())
self.header.get(&headerdef.get_headername())
}
fn parse_first_addr(&self, context: &Context, headerdef: HeaderDef) -> Option<MailAddr> {
fn parse_first_addr(&self, headerdef: HeaderDef) -> Option<MailAddr> {
if let Some(value) = self.get(headerdef.clone()) {
match mailparse::addrparse(&value) {
Ok(ref addrs) => {
return addrs.first().cloned();
}
Err(err) => {
warn!(context, "header {} parse error: {:?}", headerdef, err);
warn!(self.context, "header {} parse error: {:?}", headerdef, err);
}
}
}
None
}
fn parse_mime_recursive(
&mut self,
context: &Context,
mail: &mailparse::ParsedMail<'_>,
) -> Result<bool> {
fn parse_mime_recursive(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
if mail.ctype.params.get("protected-headers").is_some() {
if mail.ctype.mimetype == "text/rfc822-headers" {
warn!(
context,
self.context,
"Protected headers found in text/rfc822-headers attachment: Will be ignored.",
);
return Ok(false);
}
warn!(context, "Ignoring nested protected headers");
warn!(self.context, "Ignoring nested protected headers");
}
enum MimeS {
@@ -446,7 +459,7 @@ impl MimeMessage {
};
match m {
MimeS::Multiple => self.handle_multiple(context, mail),
MimeS::Multiple => self.handle_multiple(mail),
MimeS::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
@@ -454,17 +467,13 @@ impl MimeMessage {
}
let mail = mailparse::parse_mail(&raw).unwrap();
self.parse_mime_recursive(context, &mail)
self.parse_mime_recursive(&mail)
}
MimeS::Single => self.add_single_part_if_known(context, mail),
MimeS::Single => self.add_single_part_if_known(mail),
}
}
fn handle_multiple(
&mut self,
context: &Context,
mail: &mailparse::ParsedMail<'_>,
) -> Result<bool> {
fn handle_multiple(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
let mut any_part_added = false;
let mimetype = get_mime_type(mail)?.0;
match (mimetype.type_(), mimetype.subtype().as_str()) {
@@ -475,7 +484,7 @@ impl MimeMessage {
(mime::MULTIPART, "alternative") => {
for cur_data in &mail.subparts {
if get_mime_type(cur_data)?.0 == "multipart/mixed" {
any_part_added = self.parse_mime_recursive(context, cur_data)?;
any_part_added = self.parse_mime_recursive(cur_data)?;
break;
}
}
@@ -483,7 +492,7 @@ impl MimeMessage {
/* search for text/plain and add this */
for cur_data in &mail.subparts {
if get_mime_type(cur_data)?.0.type_() == mime::TEXT {
any_part_added = self.parse_mime_recursive(context, cur_data)?;
any_part_added = self.parse_mime_recursive(cur_data)?;
break;
}
}
@@ -491,7 +500,7 @@ impl MimeMessage {
if !any_part_added {
/* `text/plain` not found - use the first part */
for cur_part in &mail.subparts {
if self.parse_mime_recursive(context, cur_part)? {
if self.parse_mime_recursive(cur_part)? {
any_part_added = true;
break;
}
@@ -504,14 +513,14 @@ impl MimeMessage {
being the first one, which may not be always true ...
however, most times it seems okay. */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first)?;
any_part_added = self.parse_mime_recursive(first)?;
}
}
(mime::MULTIPART, "encrypted") => {
// we currently do not try to decrypt non-autocrypt messages
// at all. If we see an encrypted part, we set
// decrypting_failed.
let msg_body = context.stock_str(StockMessage::CantDecryptMsgBody);
let msg_body = self.context.stock_str(StockMessage::CantDecryptMsgBody);
let txt = format!("[{}]", msg_body);
let mut part = Part::default();
@@ -534,7 +543,7 @@ impl MimeMessage {
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
for background information why we use encrypted+signed) */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first)?;
any_part_added = self.parse_mime_recursive(first)?;
}
}
(mime::MULTIPART, "report") => {
@@ -542,14 +551,14 @@ impl MimeMessage {
if mail.subparts.len() >= 2 {
if let Some(report_type) = mail.ctype.params.get("report-type") {
if report_type == "disposition-notification" {
if let Some(report) = self.process_report(context, mail)? {
if let Some(report) = self.process_report(mail)? {
self.reports.push(report);
}
} else {
/* eg. `report-type=delivery-status`;
maybe we should show them as a little error icon */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first)?;
any_part_added = self.parse_mime_recursive(first)?;
}
}
}
@@ -559,7 +568,7 @@ impl MimeMessage {
// Add all parts (in fact, AddSinglePartIfKnown() later check if
// the parts are really supported)
for cur_data in mail.subparts.iter() {
if self.parse_mime_recursive(context, cur_data)? {
if self.parse_mime_recursive(cur_data)? {
any_part_added = true;
}
}
@@ -569,73 +578,64 @@ impl MimeMessage {
Ok(any_part_added)
}
fn add_single_part_if_known(
&mut self,
context: &Context,
mail: &mailparse::ParsedMail<'_>,
) -> Result<bool> {
fn add_single_part_if_known(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
// return true if a part was added
let (mime_type, msg_type) = get_mime_type(mail)?;
let raw_mime = mail.ctype.mimetype.to_lowercase();
let filename = get_attachment_filename(mail)?;
let filename = get_attachment_filename(mail);
let old_part_count = self.parts.len();
match filename {
Some(filename) => {
self.do_add_single_file_part(
context,
msg_type,
mime_type,
&raw_mime,
&mail.get_body_raw()?,
&filename,
);
}
None => {
match mime_type.type_() {
mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
warn!(context, "Missing attachment");
return Ok(false);
}
mime::TEXT | mime::HTML => {
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
Err(err) => {
warn!(context, "Invalid body parsed {:?}", err);
// Note that it's not always an error - might be no data
return Ok(false);
}
};
let (simplified_txt, is_forwarded) = if decoded_data.is_empty() {
("".into(), false)
} else {
let is_html = mime_type == mime::TEXT_HTML;
let out = if is_html {
dehtml(&decoded_data)
} else {
decoded_data.clone()
};
simplify(out, self.has_chat_version())
};
if !simplified_txt.is_empty() {
let mut part = Part::default();
part.typ = Viewtype::Text;
part.mimetype = Some(mime_type);
part.msg = simplified_txt;
part.msg_raw = Some(decoded_data);
self.do_add_single_part(part);
}
if is_forwarded {
self.is_forwarded = true;
}
}
_ => {}
if let Ok(filename) = filename {
self.do_add_single_file_part(
msg_type,
mime_type,
&raw_mime,
&mail.get_body_raw()?,
&filename,
);
} else {
match mime_type.type_() {
mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
bail!("missing attachment");
}
mime::TEXT | mime::HTML => {
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
Err(err) => {
warn!(self.context, "Invalid body parsed {:?}", err);
// Note that it's not always an error - might be no data
return Ok(false);
}
};
let (simplified_txt, is_forwarded) = if decoded_data.is_empty() {
("".into(), false)
} else {
let is_html = mime_type == mime::TEXT_HTML;
let out = if is_html {
dehtml(&decoded_data)
} else {
decoded_data.clone()
};
simplify(out, self.has_chat_version())
};
if !simplified_txt.is_empty() {
let mut part = Part::default();
part.typ = Viewtype::Text;
part.mimetype = Some(mime_type);
part.msg = simplified_txt;
part.msg_raw = Some(decoded_data);
self.do_add_single_part(part);
}
if is_forwarded {
self.is_forwarded = true;
}
}
_ => {}
}
}
@@ -645,7 +645,6 @@ impl MimeMessage {
fn do_add_single_file_part(
&mut self,
context: &Context,
msg_type: Viewtype,
mime_type: Mime,
raw_mime: &str,
@@ -660,9 +659,9 @@ impl MimeMessage {
// XXX what if somebody sends eg an "location-highlights.kml"
// attachment unrelated to location streaming?
if filename.starts_with("location") || filename.starts_with("message") {
let parsed = location::Kml::parse(context, decoded_data)
let parsed = location::Kml::parse(self.context, decoded_data)
.map_err(|err| {
warn!(context, "failed to parse kml part: {}", err);
warn!(self.context, "failed to parse kml part: {}", err);
})
.ok();
if filename.starts_with("location") {
@@ -676,17 +675,17 @@ impl MimeMessage {
/* we have a regular file attachment,
write decoded data to new blob object */
let blob = match BlobObject::create(context, filename, decoded_data) {
let blob = match BlobObject::create(self.context, filename, decoded_data) {
Ok(blob) => blob,
Err(err) => {
error!(
context,
self.context,
"Could not add blob for mime part {}, error {}", filename, err
);
return;
}
};
info!(context, "added blobfile: {:?}", blob.as_name());
info!(self.context, "added blobfile: {:?}", blob.as_name());
/* create and register Mime part referencing the new Blob object */
let mut part = Part::default();
@@ -741,7 +740,7 @@ impl MimeMessage {
pub fn get_rfc724_mid(&self) -> Option<String> {
self.get(HeaderDef::MessageId)
.and_then(|msgid| parse_message_id(msgid).ok())
.and_then(|msgid| parse_message_id(msgid))
}
fn merge_headers(headers: &mut HashMap<String, String>, fields: &[mailparse::MailHeader<'_>]) {
@@ -760,35 +759,26 @@ impl MimeMessage {
}
}
fn process_report(
&self,
context: &Context,
report: &mailparse::ParsedMail<'_>,
) -> Result<Option<Report>> {
fn process_report(&self, report: &mailparse::ParsedMail<'_>) -> Result<Option<Report>> {
// parse as mailheaders
let report_body = report.subparts[1].get_body_raw()?;
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
// must be present
if let Some(_disposition) = report_fields
.get_header_value(HeaderDef::Disposition)
.ok()
.flatten()
{
let disp = HeaderDef::Disposition.get_headername();
if let Some(_disposition) = report_fields.get_first_value(&disp).ok().flatten() {
if let Some(original_message_id) = report_fields
.get_header_value(HeaderDef::OriginalMessageId)
.get_first_value(&HeaderDef::OriginalMessageId.get_headername())
.ok()
.flatten()
.and_then(|v| parse_message_id(&v).ok())
.and_then(|v| parse_message_id(&v))
{
let additional_message_ids = report_fields
.get_header_value(HeaderDef::AdditionalMessageIds)
.get_first_value(&HeaderDef::AdditionalMessageIds.get_headername())
.ok()
.flatten()
.map_or_else(Vec::new, |v| {
v.split(' ')
.filter_map(|s| parse_message_id(s).ok())
.collect()
v.split(' ').filter_map(parse_message_id).collect()
});
return Ok(Some(Report {
@@ -798,9 +788,9 @@ impl MimeMessage {
}
}
warn!(
context,
self.context,
"ignoring unknown disposition-notification, Message-Id: {:?}",
report_fields.get_header_value(HeaderDef::MessageId).ok()
report_fields.get_first_value("Message-ID").ok()
);
Ok(None)
@@ -809,7 +799,6 @@ impl MimeMessage {
/// Handle reports (only MDNs for now)
pub fn handle_reports(
&self,
context: &Context,
from_id: u32,
sent_timestamp: i64,
server_folder: impl AsRef<str>,
@@ -824,10 +813,13 @@ impl MimeMessage {
for original_message_id in
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
{
if let Some((chat_id, msg_id)) =
message::mdn_from_ext(context, from_id, original_message_id, sent_timestamp)
{
context.call_cb(Event::MsgRead { chat_id, msg_id });
if let Some((chat_id, msg_id)) = message::mdn_from_ext(
self.context,
from_id,
original_message_id,
sent_timestamp,
) {
self.context.call_cb(Event::MsgRead { chat_id, msg_id });
mdn_recognized = true;
}
}
@@ -837,10 +829,10 @@ impl MimeMessage {
let mut param = Params::new();
param.set(Param::ServerFolder, server_folder.as_ref());
param.set_int(Param::ServerUid, server_uid as i32);
if self.has_chat_version() && context.get_config_bool(Config::MvboxMove) {
if self.has_chat_version() && self.context.get_config_bool(Config::MvboxMove) {
param.set_int(Param::AlsoMove, 1);
}
job_add(context, Action::MarkseenMdnOnImap, 0, param, 0);
job_add(self.context, Action::MarkseenMdnOnImap, 0, param, 0);
}
}
}
@@ -911,20 +903,14 @@ pub(crate) struct Report {
additional_message_ids: Vec<String>,
}
pub(crate) fn parse_message_id(value: &str) -> crate::error::Result<String> {
let ids = mailparse::msgidparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
if ids.len() == 1 {
let id = &ids[0];
if id.starts_with('<') && id.ends_with('>') {
Ok(id.chars().skip(1).take(id.len() - 2).collect())
} else {
bail!("message-ID {} is not enclosed in < and >", value);
fn parse_message_id(field: &str) -> Option<String> {
if let Ok(addrs) = mailparse::addrparse(field) {
// Assume the message id is a single id in the form of <id>
if let mailparse::MailAddr::Single(mailparse::SingleInfo { ref addr, .. }) = addrs[0] {
return Some(addr.clone());
}
} else {
bail!("could not parse message_id: {}", value);
}
None
}
fn is_known(key: &str) -> bool {
@@ -998,48 +984,45 @@ fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
false
}
/// Tries to get attachment filename.
///
/// If filename is explitictly specified in Content-Disposition, it is
/// returned. If Content-Disposition is "attachment" but filename is
/// not specified, filename is guessed. If Content-Disposition cannot
/// be parsed, returns an error.
fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<Option<String>> {
fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<String> {
// try to get file name from
// `Content-Disposition: ... filename*=...`
// or `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...`
// or `Content-Disposition: ... filename=...`
let ct = mail.get_content_disposition()?;
ensure!(
ct.disposition == DispositionType::Attachment,
"disposition not an attachment: {:?}",
ct.disposition
);
let desired_filename: Option<String> = ct
let mut desired_filename = ct
.params
.iter()
.filter(|(key, _value)| key.starts_with("filename"))
.fold(None, |acc, (_key, value)| {
if let Some(acc) = acc {
Some(acc + value)
} else {
Some(value.to_string())
}
.fold(String::new(), |mut acc, (_key, value)| {
acc += value;
acc
});
let desired_filename =
desired_filename.or_else(|| ct.params.get("name").map(|s| s.to_string()));
// If there is no filename, but part is an attachment, guess filename
if ct.disposition == DispositionType::Attachment && desired_filename.is_none() {
if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
Ok(Some(format!("file.{}", subtype,)))
} else {
bail!(
"could not determine attachment filename: {:?}",
ct.disposition
);
if desired_filename.is_empty() {
if let Some(param) = ct.params.get("name") {
// might be a wrongly encoded filename
desired_filename = param.to_string();
}
} else {
Ok(desired_filename)
}
// if there is still no filename, guess one
if desired_filename.is_empty() {
if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
desired_filename = format!("file.{}", subtype,);
} else {
bail!("could not determine filename: {:?}", ct.disposition);
}
}
Ok(desired_filename)
}
// returned addresses are normalized and lowercased.
@@ -1093,15 +1076,6 @@ mod tests {
use super::*;
use crate::test_utils::*;
impl AvatarAction {
pub fn is_change(&self) -> bool {
match self {
AvatarAction::Delete => false,
AvatarAction::Change(_) => true,
}
}
}
#[test]
fn test_dc_mimeparser_crash() {
let context = dummy_context();
@@ -1160,12 +1134,10 @@ mod tests {
fn test_get_attachment_filename() {
let raw = include_bytes!("../test-data/message/html_attach.eml");
let mail = mailparse::parse_mail(raw).unwrap();
assert!(get_attachment_filename(&mail).unwrap().is_none());
assert!(get_attachment_filename(&mail.subparts[0])
.unwrap()
.is_none());
assert!(get_attachment_filename(&mail).is_err());
assert!(get_attachment_filename(&mail.subparts[0]).is_err());
let filename = get_attachment_filename(&mail.subparts[1]).unwrap();
assert_eq!(filename, Some("test.html".to_string()))
assert_eq!(filename, "test.html")
}
#[test]
@@ -1194,13 +1166,10 @@ mod tests {
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
let of = mimeparser
.parse_first_addr(&context.ctx, HeaderDef::From_)
.unwrap();
let of = mimeparser.parse_first_addr(HeaderDef::From_).unwrap();
assert_eq!(of, mailparse::addrparse("hello@one.org").unwrap()[0]);
let of =
mimeparser.parse_first_addr(&context.ctx, HeaderDef::ChatDispositionNotificationTo);
let of = mimeparser.parse_first_addr(HeaderDef::ChatDispositionNotificationTo);
assert!(of.is_none());
}
@@ -1251,29 +1220,29 @@ mod tests {
let raw = include_bytes!("../test-data/message/mail_attach_txt.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.user_avatar, None);
assert_eq!(mimeparser.group_avatar, None);
assert_eq!(mimeparser.user_avatar, AvatarAction::None);
assert_eq!(mimeparser.group_avatar, AvatarAction::None);
let raw = include_bytes!("../test-data/message/mail_with_user_avatar.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert!(mimeparser.user_avatar.unwrap().is_change());
assert_eq!(mimeparser.group_avatar, None);
assert!(mimeparser.user_avatar.is_change());
assert_eq!(mimeparser.group_avatar, AvatarAction::None);
let raw = include_bytes!("../test-data/message/mail_with_user_avatar_deleted.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert_eq!(mimeparser.user_avatar, Some(AvatarAction::Delete));
assert_eq!(mimeparser.group_avatar, None);
assert_eq!(mimeparser.user_avatar, AvatarAction::Delete);
assert_eq!(mimeparser.group_avatar, AvatarAction::None);
let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert!(mimeparser.user_avatar.unwrap().is_change());
assert!(mimeparser.group_avatar.unwrap().is_change());
assert!(mimeparser.user_avatar.is_change());
assert!(mimeparser.group_avatar.is_change());
// if the Chat-User-Avatar header is missing, the avatar become a normal attachment
let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml");
@@ -1282,8 +1251,8 @@ mod tests {
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw.as_bytes()).unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Image);
assert_eq!(mimeparser.user_avatar, None);
assert!(mimeparser.group_avatar.unwrap().is_change());
assert_eq!(mimeparser.user_avatar, AvatarAction::None);
assert!(mimeparser.group_avatar.is_change());
}
#[test]
@@ -1500,40 +1469,4 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
&["foo@example.com", "foo@example.net"]
);
}
#[test]
fn test_parse_inline_attachment() {
let context = dummy_context();
let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC)
From: sender@example.com
To: receiver@example.com
Subject: Mail with inline attachment
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_Part_25_46172632.1581201680436"
------=_Part_25_46172632.1581201680436
Content-Type: text/plain; charset=utf-8
Hello!
------=_Part_25_46172632.1581201680436
Content-Type: application/pdf; name="some_pdf.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="some_pdf.pdf"
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
Y29kZT4+CnN0cmVhbQp4nGVOuwoCMRDs8xVbC8aZvC4Hx4Hno7ATAhZi56MTtPH33YtXiLKQ3ZnM
MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
------=_Part_25_46172632.1581201680436--
"#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
assert_eq!(
message.get_subject(),
Some("Mail with inline attachment".to_string())
);
assert_eq!(message.parts.len(), 2);
}
}

View File

@@ -4,7 +4,6 @@ use std::path::PathBuf;
use std::str;
use num_traits::FromPrimitive;
use serde::{Deserialize, Serialize};
use crate::blob::{BlobError, BlobObject};
use crate::context::Context;
@@ -13,9 +12,7 @@ use crate::message::MsgId;
use crate::mimeparser::SystemMessage;
/// Available param keys.
#[derive(
PartialEq, Eq, Debug, Clone, Copy, Hash, PartialOrd, Ord, FromPrimitive, Serialize, Deserialize,
)]
#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash, PartialOrd, Ord, FromPrimitive)]
#[repr(u8)]
pub enum Param {
/// For messages and jobs
@@ -138,7 +135,7 @@ pub enum ForcePlaintext {
/// The structure is serialized by calling `to_string()` on it.
///
/// Only for library-internal use.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Params {
inner: BTreeMap<Param, String>,
}

View File

@@ -1,6 +1,5 @@
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
use std::collections::HashSet;
use std::convert::TryFrom;
use std::fmt;
use num_traits::FromPrimitive;
@@ -8,7 +7,7 @@ use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::constants::*;
use crate::context::Context;
use crate::key::{Key, SignedPublicKey};
use crate::key::*;
use crate::sql::{self, Sql};
#[derive(Debug)]
@@ -127,7 +126,7 @@ impl<'a> Peerstate<'a> {
res.last_seen_autocrypt = message_time;
res.to_save = Some(ToSave::All);
res.prefer_encrypt = header.prefer_encrypt;
res.public_key = Some(Key::from(header.public_key.clone()));
res.public_key = Some(header.public_key.clone());
res.recalc_fingerprint();
res
@@ -138,7 +137,7 @@ impl<'a> Peerstate<'a> {
res.gossip_timestamp = message_time;
res.to_save = Some(ToSave::All);
res.gossip_key = Some(Key::from(gossip_header.public_key.clone()));
res.gossip_key = Some(gossip_header.public_key.clone());
res.recalc_fingerprint();
res
@@ -294,8 +293,8 @@ impl<'a> Peerstate<'a> {
self.to_save = Some(ToSave::All)
}
if self.public_key.as_ref() != Some(&Key::from(header.public_key.clone())) {
self.public_key = Some(Key::from(header.public_key.clone()));
if self.public_key.as_ref() != Some(&header.public_key) {
self.public_key = Some(header.public_key.clone());
self.recalc_fingerprint();
self.to_save = Some(ToSave::All);
}
@@ -310,50 +309,21 @@ impl<'a> Peerstate<'a> {
if message_time > self.gossip_timestamp {
self.gossip_timestamp = message_time;
self.to_save = Some(ToSave::Timestamps);
let hdr_key = Key::from(gossip_header.public_key.clone());
if self.gossip_key.as_ref() != Some(&hdr_key) {
self.gossip_key = Some(hdr_key);
if self.gossip_key.as_ref() != Some(&gossip_header.public_key) {
self.gossip_key = Some(gossip_header.public_key.clone());
self.recalc_fingerprint();
self.to_save = Some(ToSave::All)
}
// This is non-standard.
//
// According to Autocrypt 1.1.0 gossip headers SHOULD NOT
// contain encryption preference, but we include it into
// Autocrypt-Gossip and apply it one way (from
// "nopreference" to "mutual").
//
// This is compatible to standard clients, because they
// can't distinguish it from the case where we have
// contacted the client in the past and received this
// preference via Autocrypt header.
if self.last_seen_autocrypt == 0
&& self.prefer_encrypt == EncryptPreference::NoPreference
&& gossip_header.prefer_encrypt == EncryptPreference::Mutual
{
self.prefer_encrypt = EncryptPreference::Mutual;
self.to_save = Some(ToSave::All);
}
};
}
pub fn render_gossip_header(&self, min_verified: PeerstateVerifiedStatus) -> Option<String> {
if let Some(key) = self.peek_key(min_verified) {
// TODO: avoid cloning
let public_key = SignedPublicKey::try_from(key.clone()).ok()?;
let header = Aheader::new(
self.addr.clone(),
public_key,
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included,
// but we include it anyway to propagate encryption
// preference to new members in group chats.
if self.last_seen_autocrypt > 0 {
self.prefer_encrypt
} else {
EncryptPreference::NoPreference
},
key.clone(),
EncryptPreference::NoPreference,
);
Some(header.to_string())
} else {
@@ -362,13 +332,18 @@ impl<'a> Peerstate<'a> {
}
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Key> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(),
PeerstateVerifiedStatus::Unverified => self
.public_key
.as_ref()
.or_else(|| self.gossip_key.as_ref()),
if self.public_key.is_none() && self.gossip_key.is_none() && self.verified_key.is_none() {
return None;
}
if min_verified != PeerstateVerifiedStatus::Unverified {
return self.verified_key.as_ref();
}
if self.public_key.is_some() {
return self.public_key.as_ref();
}
self.gossip_key.as_ref()
}
pub fn set_verified(
@@ -475,8 +450,8 @@ impl<'a> Peerstate<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
@@ -484,7 +459,11 @@ mod tests {
let ctx = crate::test_utils::dummy_context();
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
let pub_key = crate::key::Key::from_base64(
include_str!("../test-data/key/public.asc"),
KeyType::Public,
)
.unwrap();
let mut peerstate = Peerstate {
context: &ctx.ctx,
@@ -524,7 +503,12 @@ mod tests {
fn test_peerstate_double_create() {
let ctx = crate::test_utils::dummy_context();
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
let pub_key = crate::key::Key::from_base64(
include_str!("../test-data/key/public.asc"),
KeyType::Public,
)
.unwrap();
let peerstate = Peerstate {
context: &ctx.ctx,
@@ -558,7 +542,11 @@ mod tests {
let ctx = crate::test_utils::dummy_context();
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
let pub_key = crate::key::Key::from_base64(
include_str!("../test-data/key/public.asc"),
KeyType::Public,
)
.unwrap();
let mut peerstate = Peerstate {
context: &ctx.ctx,

View File

@@ -16,8 +16,6 @@ use pgp::types::{
};
use rand::{thread_rng, CryptoRng, Rng};
use crate::constants::KeyGenType;
use crate::dc_tools::EmailAddress;
use crate::error::Result;
use crate::key::*;
use crate::keyring::*;
@@ -113,53 +111,12 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
Ok((typ, headers, bytes))
}
/// Error with generating a PGP keypair.
///
/// Most of these are likely coding errors rather than user errors
/// since all variability is hardcoded.
#[derive(Fail, Debug)]
#[fail(display = "PgpKeygenError: {}", message)]
pub(crate) struct PgpKeygenError {
message: String,
#[cause]
cause: failure::Error,
backtrace: failure::Backtrace,
}
impl PgpKeygenError {
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
Self {
message: message.into(),
cause: cause.into(),
backtrace: failure::Backtrace::new(),
}
}
}
/// A PGP keypair.
///
/// This has it's own struct to be able to keep the public and secret
/// keys together as they are one unit.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct KeyPair {
pub addr: EmailAddress,
pub public: SignedPublicKey,
pub secret: SignedSecretKey,
}
/// Create a new key pair.
pub(crate) fn create_keypair(
addr: EmailAddress,
keygen_type: KeyGenType,
) -> std::result::Result<KeyPair, PgpKeygenError> {
let (secret_key_type, public_key_type) = match keygen_type {
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
};
pub fn create_keypair(addr: impl AsRef<str>) -> Option<(Key, Key)> {
let user_id = format!("<{}>", addr.as_ref());
let user_id = format!("<{}>", addr);
let key_params = SecretKeyParamsBuilder::default()
.key_type(secret_key_type)
.key_type(PgpKeyType::Rsa(2048))
.can_create_certificates(true)
.can_sign(true)
.primary_user_id(user_id)
@@ -182,36 +139,27 @@ pub(crate) fn create_keypair(
])
.subkey(
SubkeyParamsBuilder::default()
.key_type(public_key_type)
.key_type(PgpKeyType::Rsa(2048))
.can_encrypt(true)
.passphrase(None)
.build()
.unwrap(),
)
.build()
.map_err(|err| PgpKeygenError::new("invalid key params", failure::err_msg(err)))?;
let key = key_params
.generate()
.map_err(|err| PgpKeygenError::new("invalid params", err))?;
.expect("invalid key params");
let key = key_params.generate().expect("invalid params");
let private_key = key.sign(|| "".into()).expect("failed to sign secret key");
let public_key = private_key.public_key();
let public_key = public_key
.sign(&private_key, || "".into())
.map_err(|err| PgpKeygenError::new("failed to sign public key", err))?;
.expect("failed to sign public key");
private_key
.verify()
.map_err(|err| PgpKeygenError::new("invalid private key generated", err))?;
public_key
.verify()
.map_err(|err| PgpKeygenError::new("invalid public key generated", err))?;
private_key.verify().expect("invalid private key generated");
public_key.verify().expect("invalid public key generated");
Ok(KeyPair {
addr,
public: public_key,
secret: private_key,
})
Some((Key::Public(public_key), Key::Secret(private_key)))
}
/// Select public key or subkey to use for encryption.
@@ -363,8 +311,6 @@ pub fn symm_decrypt<T: std::io::Read + std::io::Seek>(
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use lazy_static::lazy_static;
#[test]
fn test_split_armored_data_1() {
@@ -392,269 +338,4 @@ mod tests {
assert!(!base64.is_empty());
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
}
#[test]
#[ignore] // is too expensive
fn test_create_keypair() {
let keypair0 = create_keypair(
EmailAddress::new("foo@bar.de").unwrap(),
KeyGenType::Default,
)
.unwrap();
let keypair1 = create_keypair(
EmailAddress::new("two@zwo.de").unwrap(),
KeyGenType::Default,
)
.unwrap();
assert_ne!(keypair0.public, keypair1.public);
}
/// [Key] objects to use in tests.
struct TestKeys {
alice_secret: Key,
alice_public: Key,
bob_secret: Key,
bob_public: Key,
}
impl TestKeys {
fn new() -> TestKeys {
let alice = alice_keypair();
let bob = bob_keypair();
TestKeys {
alice_secret: Key::from(alice.secret.clone()),
alice_public: Key::from(alice.public.clone()),
bob_secret: Key::from(bob.secret.clone()),
bob_public: Key::from(bob.public.clone()),
}
}
}
/// The original text of [CTEXT_SIGNED]
static CLEARTEXT: &[u8] = b"This is a test";
lazy_static! {
/// Initialised [TestKeys] for tests.
static ref KEYS: TestKeys = TestKeys::new();
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
static ref CTEXT_SIGNED: String = {
let mut keyring = Keyring::default();
keyring.add_owned(KEYS.alice_public.clone());
keyring.add_ref(&KEYS.bob_public);
pk_encrypt(CLEARTEXT, &keyring, Some(&KEYS.alice_secret)).unwrap()
};
/// A cyphertext encrypted to Alice & Bob, not signed.
static ref CTEXT_UNSIGNED: String = {
let mut keyring = Keyring::default();
keyring.add_owned(KEYS.alice_public.clone());
keyring.add_ref(&KEYS.bob_public);
pk_encrypt(CLEARTEXT, &keyring, None).unwrap()
};
}
#[test]
fn test_encrypt_signed() {
assert!(!CTEXT_SIGNED.is_empty());
assert!(CTEXT_SIGNED.starts_with("-----BEGIN PGP MESSAGE-----"));
}
#[test]
fn test_encrypt_unsigned() {
assert!(!CTEXT_UNSIGNED.is_empty());
assert!(CTEXT_UNSIGNED.starts_with("-----BEGIN PGP MESSAGE-----"));
}
#[test]
fn test_decrypt_singed() {
// Check decrypting as Alice
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.alice_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.map_err(|err| println!("{:?}", err))
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.map_err(|err| println!("{:?}", err))
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
}
#[test]
fn test_decrypt_no_sig_check() {
let mut keyring = Keyring::default();
keyring.add_ref(&KEYS.alice_secret);
let empty_keyring = Keyring::default();
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&keyring,
&empty_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[test]
fn test_decrypt_signed_no_key() {
// The validation does not have the public key of the signer.
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.bob_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[test]
fn test_decrypt_unsigned() {
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let sig_check_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.alice_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_UNSIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[test]
fn test_decrypt_signed_no_sigret() {
// Check decrypting signed cyphertext without providing the HashSet for signatures.
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public);
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
None,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
}
fn test_encrypt_decrypt_fuzz(key_type: KeyGenType) {
// sending messages from Alice to Bob
let alice = create_keypair(EmailAddress::new("a@e.org").unwrap(), key_type).unwrap();
let bob = create_keypair(EmailAddress::new("b@e.org").unwrap(), key_type).unwrap();
let alice_secret = Key::from(alice.secret.clone());
let alice_public = Key::from(alice.public.clone());
let bob_secret = Key::from(bob.secret.clone());
let bob_public = Key::from(bob.public.clone());
let plain: &[u8] = b"just a test";
let mut encr_keyring = Keyring::default();
encr_keyring.add_ref(&bob_public);
let mut decr_keyring = Keyring::default();
decr_keyring.add_ref(&bob_secret);
let mut validate_keyring = Keyring::default();
validate_keyring.add_ref(&alice_public);
for _ in 0..1000 {
let ctext = pk_encrypt(plain.as_ref(), &encr_keyring, Some(&alice_secret)).unwrap();
let plain2 =
pk_decrypt(ctext.as_ref(), &decr_keyring, &validate_keyring, None).unwrap();
assert_eq!(plain2, plain);
}
}
#[test]
#[ignore]
fn test_encrypt_decrypt_fuzz_rsa() {
test_encrypt_decrypt_fuzz(KeyGenType::Rsa2048)
}
#[test]
fn test_encrypt_decrypt_fuzz_ecc() {
test_encrypt_decrypt_fuzz(KeyGenType::Ed25519)
}
#[test]
fn test_encrypt_decrypt_fuzz_alice() -> Result<()> {
let plain: &[u8] = b"just a test";
let lit_msg = Message::new_literal_bytes("", plain);
let mut rng = thread_rng();
let enc_key =
SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
.unwrap();
let enc_subkey = &enc_key.public_subkeys[0];
assert!(enc_subkey.is_encryption_key());
let pkeys: Vec<&SignedPublicSubKey> = vec![&enc_subkey];
let skey = SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
.unwrap();
let skeys: Vec<&SignedSecretKey> = vec![&skey];
for _ in 0..1000 {
let msg = lit_msg.encrypt_to_keys(
&mut rng,
pgp::crypto::sym::SymmetricKeyAlgorithm::AES128,
&pkeys[..],
)?;
let ctext = msg.to_armored_string(None)?;
println!("{}", ctext);
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");
let dec_msg = &msgs[0];
let plain2: Vec<u8> = match dec_msg.get_content()? {
Some(content) => content,
None => bail!("Decrypted message is empty"),
};
assert_eq!(plain2, plain);
}
Ok(())
}
}

View File

@@ -1,365 +0,0 @@
// file generated by src/provider/update.py
use crate::provider::Protocol::*;
use crate::provider::Socket::*;
use crate::provider::UsernamePattern::*;
use crate::provider::*;
use std::collections::HashMap;
lazy_static::lazy_static! {
// aktivix.org.md: aktivix.org
static ref P_AKTIVIX_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/aktivix-org",
server: vec![
Server { protocol: IMAP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 25, username_pattern: EMAIL },
],
};
// aol.md: aol.com
static ref P_AOL: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "To log in to AOL with Delta Chat, you need to set up an app password in the AOL web interface.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/aol",
server: vec![
],
};
// autistici.org.md: autistici.org
static ref P_AUTISTICI_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/autistici-org",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "mail.autistici.org", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.autistici.org", port: 465, username_pattern: EMAIL },
],
};
// bluewin.ch.md: bluewin.ch
static ref P_BLUEWIN_CH: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/bluewin-ch",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imaps.bluewin.ch", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtpauths.bluewin.ch", port: 465, username_pattern: EMAIL },
],
};
// comcast.md: xfinity.com, comcast.net
// - skipping provider with status OK and no special things to do
// dismail.de.md: dismail.de
// - skipping provider with status OK and no special things to do
// disroot.md: disroot.org
// - skipping provider with status OK and no special things to do
// example.com.md: example.com, example.org
static ref P_EXAMPLE_COM: Provider = Provider {
status: Status::BROKEN,
before_login_hint: "Hush this provider doesn't exist!",
after_login_hint: "This provider doesn't really exist, so you can't use it :/ If you need an email provider for Delta Chat, take a look at providers.delta.chat!",
overview_page: "https://providers.delta.chat/example-com",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.example.com", port: 1337, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.example.com", port: 1337, username_pattern: EMAIL },
],
};
// fastmail.md: fastmail.com
static ref P_FASTMAIL: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "You must create an app-specific password for Delta Chat before you can log in.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/fastmail",
server: vec![
],
};
// freenet.de.md: freenet.de
static ref P_FREENET_DE: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/freenet-de",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "mx.freenet.de", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "mx.freenet.de", port: 587, username_pattern: EMAIL },
],
};
// gmail.md: gmail.com, googlemail.com
static ref P_GMAIL: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "For Gmail accounts, you need to create an app-password if you have \"2-Step Verification\" enabled. If this setting is not available, you need to enable \"less secure apps\".",
after_login_hint: "",
overview_page: "https://providers.delta.chat/gmail",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.gmail.com", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.gmail.com", port: 465, username_pattern: EMAIL },
],
};
// gmx.net.md: gmx.net, gmx.de, gmx.at, gmx.ch, gmx.org, gmx.eu, gmx.info, gmx.biz, gmx.com
static ref P_GMX_NET: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "You must allow IMAP access to your account before you can login.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/gmx-net",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.gmx.net", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "mail.gmx.net", port: 465, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "mail.gmx.net", port: 587, username_pattern: EMAIL },
],
};
// i.ua.md: i.ua
// - skipping provider with status OK and no special things to do
// icloud.md: icloud.com, me.com, mac.com
static ref P_ICLOUD: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "You must create an app-specific password for Delta Chat before you can login.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/icloud",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.mail.me.com", port: 993, username_pattern: EMAILLOCALPART },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.mail.me.com", port: 587, username_pattern: EMAIL },
],
};
// kolst.com.md: kolst.com
// - skipping provider with status OK and no special things to do
// kontent.com.md: kontent.com
// - skipping provider with status OK and no special things to do
// mail.ru.md: mail.ru, inbox.ru, bk.ru, list.ru
// - skipping provider with status OK and no special things to do
// mailbox.org.md: mailbox.org, secure.mailbox.org
// - skipping provider with status OK and no special things to do
// nauta.cu.md: nauta.cu
static ref P_NAUTA_CU: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "Atención - con nauta.cu, puede enviar mensajes sólo a un máximo de 20 personas a la vez. En grupos más grandes, no puede enviar mensajes o abandonar el grupo.",
overview_page: "https://providers.delta.chat/nauta-cu",
server: vec![
Server { protocol: IMAP, socket: STARTTLS, hostname: "imap.nauta.cu", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.nauta.cu", port: 25, username_pattern: EMAIL },
],
};
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
static ref P_OUTLOOK_COM: Provider = Provider {
status: Status::BROKEN,
before_login_hint: "Outlook.com email addresses will not work as expected as these servers remove some important transport information. Hopefully sooner or later there will be a fix, for now we suggest to use another email address.",
after_login_hint: "Outlook.com email addresses will not work as expected as these servers remove some important transport information. Unencrypted 1-on-1 chats kind of work, but groups and encryption don't. Hopefully sooner or later there will be a fix, for now we suggest to use another email address.",
overview_page: "https://providers.delta.chat/outlook-com",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap-mail.outlook.com", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp-mail.outlook.com", port: 587, username_pattern: EMAIL },
],
};
// posteo.md: posteo.de
static ref P_POSTEO: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/posteo",
server: vec![
Server { protocol: IMAP, socket: STARTTLS, hostname: "posteo.de", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "posteo.de", port: 587, username_pattern: EMAIL },
],
};
// protonmail.md: protonmail.com, protonmail.ch
static ref P_PROTONMAIL: Provider = Provider {
status: Status::BROKEN,
before_login_hint: "Protonmail does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Protonmail.",
after_login_hint: "To use Delta Chat with Protonmail, the IMAP bridge must be running in the background. If you have connectivity issues, double check whether it works as expected.",
overview_page: "https://providers.delta.chat/protonmail",
server: vec![
],
};
// riseup.net.md: riseup.net
// - skipping provider with status OK and no special things to do
// rogers.com.md: rogers.com
// - skipping provider with status OK and no special things to do
// t-online.md: t-online.de, magenta.de
static ref P_T_ONLINE: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "To use Delta Chat with a T-Online email address, you need to create an app password in the web interface.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/t-online",
server: vec![
],
};
// testrun.md: testrun.org
static ref P_TESTRUN: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/testrun",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "testrun.org", port: 993, username_pattern: EMAIL },
Server { protocol: IMAP, socket: STARTTLS, hostname: "testrun.org", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "testrun.org", port: 587, username_pattern: EMAIL },
],
};
// tiscali.it.md: tiscali.it
static ref P_TISCALI_IT: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/tiscali-it",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.tiscali.it", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.tiscali.it", port: 465, username_pattern: EMAIL },
],
};
// ukr.net.md: ukr.net
// - skipping provider with status OK and no special things to do
// vfemail.md: vfemail.net
// - skipping provider with status OK and no special things to do
// web.de.md: web.de, email.de, flirt.ms, hallo.ms, kuss.ms, love.ms, magic.ms, singles.ms, cool.ms, kanzler.ms, okay.ms, party.ms, pop.ms, stars.ms, techno.ms, clever.ms, deutschland.ms, genial.ms, ich.ms, online.ms, smart.ms, wichtig.ms, action.ms, fussball.ms, joker.ms, planet.ms, power.ms
static ref P_WEB_DE: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "You must allow IMAP access to your account before you can login.",
after_login_hint: "Note: if you have your web.de spam settings too strict, you won't receive contact requests from new people. If you want to receive contact requests, you should disable the \"3-Wege-Spamschutz\" in the web.de settings. Read how: https://hilfe.web.de/email/spam-und-viren/spamschutz-einstellungen.html",
overview_page: "https://providers.delta.chat/web-de",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.web.de", port: 993, username_pattern: EMAILLOCALPART },
Server { protocol: IMAP, socket: STARTTLS, hostname: "imap.web.de", port: 143, username_pattern: EMAILLOCALPART },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.web.de", port: 587, username_pattern: EMAILLOCALPART },
],
};
// yahoo.md: yahoo.com, yahoo.de, yahoo.it, yahoo.fr, yahoo.es, yahoo.se, yahoo.co.uk, yahoo.co.nz, yahoo.com.au, yahoo.com.ar, yahoo.com.br, yahoo.com.mx, ymail.com, rocketmail.com, yahoodns.net
static ref P_YAHOO: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "To use Delta Chat with your Yahoo email address you have to allow \"less secure apps\" in the Yahoo webinterface.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/yahoo",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.mail.yahoo.com", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.mail.yahoo.com", port: 465, username_pattern: EMAIL },
],
};
// yandex.ru.md: yandex.ru, yandex.com
// - skipping provider with status OK and no special things to do
// ziggo.nl.md: ziggo.nl
static ref P_ZIGGO_NL: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/ziggo-nl",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.ziggo.nl", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.ziggo.nl", port: 587, username_pattern: EMAIL },
],
};
pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [
("aktivix.org", &*P_AKTIVIX_ORG),
("aol.com", &*P_AOL),
("autistici.org", &*P_AUTISTICI_ORG),
("bluewin.ch", &*P_BLUEWIN_CH),
("example.com", &*P_EXAMPLE_COM),
("example.org", &*P_EXAMPLE_COM),
("fastmail.com", &*P_FASTMAIL),
("freenet.de", &*P_FREENET_DE),
("gmail.com", &*P_GMAIL),
("googlemail.com", &*P_GMAIL),
("gmx.net", &*P_GMX_NET),
("gmx.de", &*P_GMX_NET),
("gmx.at", &*P_GMX_NET),
("gmx.ch", &*P_GMX_NET),
("gmx.org", &*P_GMX_NET),
("gmx.eu", &*P_GMX_NET),
("gmx.info", &*P_GMX_NET),
("gmx.biz", &*P_GMX_NET),
("gmx.com", &*P_GMX_NET),
("icloud.com", &*P_ICLOUD),
("me.com", &*P_ICLOUD),
("mac.com", &*P_ICLOUD),
("nauta.cu", &*P_NAUTA_CU),
("hotmail.com", &*P_OUTLOOK_COM),
("outlook.com", &*P_OUTLOOK_COM),
("office365.com", &*P_OUTLOOK_COM),
("outlook.com.tr", &*P_OUTLOOK_COM),
("live.com", &*P_OUTLOOK_COM),
("posteo.de", &*P_POSTEO),
("protonmail.com", &*P_PROTONMAIL),
("protonmail.ch", &*P_PROTONMAIL),
("t-online.de", &*P_T_ONLINE),
("magenta.de", &*P_T_ONLINE),
("testrun.org", &*P_TESTRUN),
("tiscali.it", &*P_TISCALI_IT),
("web.de", &*P_WEB_DE),
("email.de", &*P_WEB_DE),
("flirt.ms", &*P_WEB_DE),
("hallo.ms", &*P_WEB_DE),
("kuss.ms", &*P_WEB_DE),
("love.ms", &*P_WEB_DE),
("magic.ms", &*P_WEB_DE),
("singles.ms", &*P_WEB_DE),
("cool.ms", &*P_WEB_DE),
("kanzler.ms", &*P_WEB_DE),
("okay.ms", &*P_WEB_DE),
("party.ms", &*P_WEB_DE),
("pop.ms", &*P_WEB_DE),
("stars.ms", &*P_WEB_DE),
("techno.ms", &*P_WEB_DE),
("clever.ms", &*P_WEB_DE),
("deutschland.ms", &*P_WEB_DE),
("genial.ms", &*P_WEB_DE),
("ich.ms", &*P_WEB_DE),
("online.ms", &*P_WEB_DE),
("smart.ms", &*P_WEB_DE),
("wichtig.ms", &*P_WEB_DE),
("action.ms", &*P_WEB_DE),
("fussball.ms", &*P_WEB_DE),
("joker.ms", &*P_WEB_DE),
("planet.ms", &*P_WEB_DE),
("power.ms", &*P_WEB_DE),
("yahoo.com", &*P_YAHOO),
("yahoo.de", &*P_YAHOO),
("yahoo.it", &*P_YAHOO),
("yahoo.fr", &*P_YAHOO),
("yahoo.es", &*P_YAHOO),
("yahoo.se", &*P_YAHOO),
("yahoo.co.uk", &*P_YAHOO),
("yahoo.co.nz", &*P_YAHOO),
("yahoo.com.au", &*P_YAHOO),
("yahoo.com.ar", &*P_YAHOO),
("yahoo.com.br", &*P_YAHOO),
("yahoo.com.mx", &*P_YAHOO),
("ymail.com", &*P_YAHOO),
("rocketmail.com", &*P_YAHOO),
("yahoodns.net", &*P_YAHOO),
("ziggo.nl", &*P_ZIGGO_NL),
].iter().copied().collect();
}

View File

@@ -1,146 +0,0 @@
//! [Provider database](https://providers.delta.chat/) module
mod data;
use crate::dc_tools::EmailAddress;
use crate::provider::data::PROVIDER_DATA;
#[derive(Debug, Copy, Clone, PartialEq, ToPrimitive)]
#[repr(u8)]
pub enum Status {
OK = 1,
PREPARATION = 2,
BROKEN = 3,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Protocol {
SMTP = 1,
IMAP = 2,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Socket {
STARTTLS = 1,
SSL = 2,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum UsernamePattern {
EMAIL = 1,
EMAILLOCALPART = 2,
}
#[derive(Debug)]
pub struct Server {
pub protocol: Protocol,
pub socket: Socket,
pub hostname: &'static str,
pub port: u16,
pub username_pattern: UsernamePattern,
}
impl Server {
pub fn apply_username_pattern(&self, addr: String) -> String {
match self.username_pattern {
UsernamePattern::EMAIL => addr,
UsernamePattern::EMAILLOCALPART => {
if let Some(at) = addr.find('@') {
return addr.split_at(at).0.to_string();
}
addr
}
}
}
}
#[derive(Debug)]
pub struct Provider {
pub status: Status,
pub before_login_hint: &'static str,
pub after_login_hint: &'static str,
pub overview_page: &'static str,
pub server: Vec<Server>,
}
impl Provider {
pub fn get_server(&self, protocol: Protocol) -> Option<&Server> {
for record in self.server.iter() {
if record.protocol == protocol {
return Some(record);
}
}
None
}
pub fn get_imap_server(&self) -> Option<&Server> {
self.get_server(Protocol::IMAP)
}
pub fn get_smtp_server(&self) -> Option<&Server> {
self.get_server(Protocol::SMTP)
}
}
pub fn get_provider_info(addr: &str) -> Option<&Provider> {
let domain = match addr.parse::<EmailAddress>() {
Ok(addr) => addr.domain,
Err(_err) => return None,
}
.to_lowercase();
if let Some(provider) = PROVIDER_DATA.get(domain.as_str()) {
return Some(*provider);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_provider_info_unexistant() {
let provider = get_provider_info("user@unexistant.org");
assert!(provider.is_none());
}
#[test]
fn test_get_provider_info_mixed_case() {
let provider = get_provider_info("uSer@nAUta.Cu").unwrap();
assert!(provider.status == Status::OK);
}
#[test]
fn test_get_provider_info() {
let provider = get_provider_info("nauta.cu"); // this is no email address
assert!(provider.is_none());
let provider = get_provider_info("user@nauta.cu").unwrap();
assert!(provider.status == Status::OK);
let server = provider.get_imap_server().unwrap();
assert_eq!(server.protocol, Protocol::IMAP);
assert_eq!(server.socket, Socket::STARTTLS);
assert_eq!(server.hostname, "imap.nauta.cu");
assert_eq!(server.port, 143);
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
let server = provider.get_smtp_server().unwrap();
assert_eq!(server.protocol, Protocol::SMTP);
assert_eq!(server.socket, Socket::STARTTLS);
assert_eq!(server.hostname, "smtp.nauta.cu");
assert_eq!(server.port, 25);
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
let provider = get_provider_info("user@gmail.com").unwrap();
assert!(provider.status == Status::PREPARATION);
assert!(!provider.before_login_hint.is_empty());
assert!(!provider.overview_page.is_empty());
let provider = get_provider_info("user@googlemail.com").unwrap();
assert!(provider.status == Status::PREPARATION);
}
}

View File

@@ -1,148 +0,0 @@
#!/usr/bin/env python3
# if the yaml import fails, run "pip install pyyaml"
import sys
import os
import yaml
out_all = ""
out_domains = ""
domains_dict = {}
def cleanstr(s):
s = s.strip()
s = s.replace("\n", " ")
s = s.replace("\\", "\\\\")
s = s.replace("\"", "\\\"")
return s
def file2varname(f):
f = f[f.rindex("/")+1:].replace(".md", "")
f = f.replace(".", "_")
f = f.replace("-", "_")
return "P_" + f.upper()
def file2url(f):
f = f[f.rindex("/")+1:].replace(".md", "")
f = f.replace(".", "-")
return "https://providers.delta.chat/" + f
def process_data(data, file):
status = data.get("status", "")
if status != "OK" and status != "PREPARATION" and status != "BROKEN":
raise TypeError("bad status")
comment = ""
domains = ""
if not "domains" in data:
raise TypeError("no domains found")
for domain in data["domains"]:
domain = cleanstr(domain)
if domain == "" or domain.count(".") < 1 or domain.lower() != domain:
raise TypeError("bad domain: " + domain)
global domains_dict
if domains_dict.get(domain, False):
raise TypeError("domain used twice: " + domain)
domains_dict[domain] = True
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
comment += domain + ", "
server = ""
has_imap = False
has_smtp = False
if "server" in data:
for s in data["server"]:
hostname = cleanstr(s.get("hostname", ""))
port = int(s.get("port", ""))
if hostname == "" or hostname.count(".") < 1 or port <= 0:
raise TypeError("bad hostname or port")
protocol = s.get("type", "").upper()
if protocol == "IMAP":
has_imap = True
elif protocol == "SMTP":
has_smtp = True
else:
raise TypeError("bad protocol")
socket = s.get("socket", "").upper()
if socket != "STARTTLS" and socket != "SSL":
raise TypeError("bad socket")
username_pattern = s.get("username_pattern", "EMAIL").upper()
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
raise TypeError("bad username pattern")
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern + " },\n")
provider = ""
before_login_hint = cleanstr(data.get("before_login_hint", ""))
after_login_hint = cleanstr(data.get("after_login_hint", ""))
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
provider += " static ref " + file2varname(file) + ": Provider = Provider {\n"
provider += " status: Status::" + status + ",\n"
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
provider += " overview_page: \"" + file2url(file) + "\",\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " };\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")
if status != "OK" and before_login_hint == "":
raise TypeError("status PREPARATION or BROKEN requires before_login_hint: " + file)
# finally, add the provider
global out_all, out_domains
out_all += " // " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
if status == "OK" and before_login_hint == "" and after_login_hint == "" and server == "":
out_all += " // - skipping provider with status OK and no special things to do\n\n"
else:
out_all += provider
out_domains += domains
def process_file(file):
print("processing file: " + file, file=sys.stderr)
with open(file) as f:
# load_all() loads "---"-separated yamls -
# by coincidence, this is also the frontmatter separator :)
data = next(yaml.load_all(f, Loader=yaml.SafeLoader))
process_data(data, file)
def process_dir(dir):
print("processing directory: " + dir, file=sys.stderr)
files = [f for f in os.listdir(dir) if f.endswith(".md")]
files.sort()
for f in files:
process_file(os.path.join(dir, f))
if __name__ == "__main__":
if len(sys.argv) < 2:
raise SystemExit("usage: update.py DIR_WITH_MD_FILES > data.rs")
out_all = ("// file generated by src/provider/update.py\n\n"
"use crate::provider::Protocol::*;\n"
"use crate::provider::Socket::*;\n"
"use crate::provider::UsernamePattern::*;\n"
"use crate::provider::*;\n"
"use std::collections::HashMap;\n\n"
"lazy_static::lazy_static! {\n\n")
process_dir(sys.argv[1])
out_all += " pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [\n"
out_all += out_domains;
out_all += " ].iter().copied().collect();\n}"
print(out_all)

View File

@@ -4,21 +4,17 @@ use lazy_static::lazy_static;
use percent_encoding::percent_decode_str;
use crate::chat;
use crate::config::*;
use crate::constants::Blocked;
use crate::contact::*;
use crate::context::Context;
use crate::error::Error;
use crate::key::dc_format_fingerprint;
use crate::key::dc_normalize_fingerprint;
use crate::key::*;
use crate::lot::{Lot, LotState};
use crate::param::*;
use crate::peerstate::*;
use reqwest::Url;
use serde::Deserialize;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
const MAILTO_SCHEME: &str = "mailto:";
const MATMSG_SCHEME: &str = "MATMSG:";
const VCARD_SCHEME: &str = "BEGIN:VCARD";
@@ -47,8 +43,6 @@ pub fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
if qr.starts_with(OPENPGP4FPR_SCHEME) {
decode_openpgp(context, qr)
} else if qr.starts_with(DCACCOUNT_SCHEME) {
decode_account(context, qr)
} else if qr.starts_with(MAILTO_SCHEME) {
decode_mailto(context, qr)
} else if qr.starts_with(SMTP_SCHEME) {
@@ -185,73 +179,6 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
lot
}
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
fn decode_account(_context: &Context, qr: &str) -> Lot {
let payload = &qr[DCACCOUNT_SCHEME.len()..];
let mut lot = Lot::new();
if let Ok(url) = Url::parse(payload) {
if url.scheme() == "https" {
lot.state = LotState::QrAccount;
lot.text1 = url.host_str().map(|x| x.to_string());
} else {
lot.state = LotState::QrError;
lot.text1 = Some(format!("Bad scheme for account url: {}", payload));
}
} else {
lot.state = LotState::QrError;
lot.text1 = Some(format!("Invalid account url: {}", payload));
}
lot
}
#[derive(Debug, Deserialize)]
struct CreateAccountResponse {
email: String,
password: String,
}
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
/// download additional information from the contained url and set the parameters.
/// on success, a configure::configure() should be able to log in to the account
pub fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
let response = reqwest::blocking::Client::new().post(url_str).send();
if response.is_err() {
return Err(format_err!(
"Cannot create account, request to {} failed",
url_str
));
}
let response = response.unwrap();
if !response.status().is_success() {
return Err(format_err!(
"Request to {} unsuccessful: {:?}",
url_str,
response
));
}
let parsed: reqwest::Result<CreateAccountResponse> = response.json();
if parsed.is_err() {
return Err(format_err!(
"Failed to parse JSON response from {}: error: {:?}",
url_str,
parsed
));
}
println!("response: {:?}", &parsed);
let parsed = parsed.unwrap();
context.set_config(Config::Addr, Some(&parsed.email))?;
context.set_config(Config::MailPw, Some(&parsed.password))?;
Ok(())
}
/// Extract address for the mailto scheme.
///
/// Scheme: `mailto:addr...?subject=...&body=..`
@@ -566,28 +493,4 @@ mod tests {
assert_eq!(res.get_state(), LotState::QrError);
assert_eq!(res.get_id(), 0);
}
#[test]
fn test_decode_account() {
let ctx = dummy_context();
let res = check_qr(
&ctx.ctx,
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
);
assert_eq!(res.get_state(), LotState::QrAccount);
assert_eq!(res.get_text1().unwrap(), "example.org");
}
#[test]
fn test_decode_account_bad_scheme() {
let ctx = dummy_context();
let res = check_qr(
&ctx.ctx,
"DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
);
assert_eq!(res.get_state(), LotState::QrError);
assert!(res.get_text1().is_some());
}
}

View File

@@ -12,7 +12,7 @@ use crate::e2ee::*;
use crate::error::Error;
use crate::events::Event;
use crate::headerdef::HeaderDef;
use crate::key::{dc_normalize_fingerprint, Key};
use crate::key::*;
use crate::lot::LotState;
use crate::message::Message;
use crate::mimeparser::*;
@@ -485,7 +485,7 @@ pub(crate) fn handle_securejoin_handshake(
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
let auth = get_qr_attr!(context, auth).to_string();
if !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice) {
if !encrypted_and_signed(mime_message, &scanned_fingerprint_of_alice) {
could_not_establish_secure_connection(
context,
contact_chat_id,
@@ -548,7 +548,7 @@ pub(crate) fn handle_securejoin_handshake(
return Ok(HandshakeMessage::Ignore);
}
};
if !encrypted_and_signed(context, mime_message, &fingerprint) {
if !encrypted_and_signed(mime_message, &fingerprint) {
could_not_establish_secure_connection(
context,
contact_chat_id,
@@ -675,7 +675,7 @@ pub(crate) fn handle_securejoin_handshake(
true
};
if vg_expect_encrypted
&& !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice)
&& !encrypted_and_signed(mime_message, &scanned_fingerprint_of_alice)
{
could_not_establish_secure_connection(
context,
@@ -830,26 +830,22 @@ fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Res
* Tools: Misc.
******************************************************************************/
fn encrypted_and_signed(
context: &Context,
mimeparser: &MimeMessage,
expected_fingerprint: impl AsRef<str>,
) -> bool {
fn encrypted_and_signed(mimeparser: &MimeMessage, expected_fingerprint: impl AsRef<str>) -> bool {
if !mimeparser.was_encrypted() {
warn!(context, "Message not encrypted.",);
warn!(mimeparser.context, "Message not encrypted.",);
false
} else if mimeparser.signatures.is_empty() {
warn!(context, "Message not signed.",);
warn!(mimeparser.context, "Message not signed.",);
false
} else if expected_fingerprint.as_ref().is_empty() {
warn!(context, "Fingerprint for comparison missing.",);
warn!(mimeparser.context, "Fingerprint for comparison missing.",);
false
} else if !mimeparser
.signatures
.contains(expected_fingerprint.as_ref())
{
warn!(
context,
mimeparser.context,
"Message does not match expected fingerprint {}.",
expected_fingerprint.as_ref(),
);

View File

@@ -2,7 +2,7 @@
pub mod send;
use std::time::{Duration, Instant};
use std::time::Duration;
use async_smtp::smtp::client::net::*;
use async_smtp::*;
@@ -53,14 +53,8 @@ pub type Result<T> = std::result::Result<T, Error>;
pub struct Smtp {
#[debug_stub(some = "SmtpTransport")]
transport: Option<smtp::SmtpTransport>,
/// Email address we are sending from.
from: Option<EmailAddress>,
/// Timestamp of last successful send/receive network interaction
/// (eg connect or send succeeded). On initialization and disconnect
/// it is set to None.
last_success: Option<Instant>,
}
impl Smtp {
@@ -74,17 +68,6 @@ impl Smtp {
if let Some(mut transport) = self.transport.take() {
async_std::task::block_on(transport.close()).ok();
}
self.last_success = None;
}
/// Return true if smtp was connected but is not known to
/// have been successfully used the last 60 seconds
pub fn has_maybe_stale_connection(&self) -> bool {
if let Some(last_success) = self.last_success {
Instant::now().duration_since(last_success).as_secs() > 60
} else {
false
}
}
/// Check whether we are connected.
@@ -178,7 +161,6 @@ impl Smtp {
trans.connect().await.map_err(Error::ConnectionFailure)?;
self.transport = Some(trans);
self.last_success = Some(Instant::now());
context.call_cb(Event::SmtpConnected(format!(
"SMTP-LOGIN as {} ok",
lp.send_user,

View File

@@ -53,8 +53,6 @@ impl Smtp {
"Message len={} was smtp-sent to {}",
message_len, recipients_display
)));
self.last_success = Some(std::time::Instant::now());
Ok(())
} else {
warn!(

View File

@@ -113,16 +113,16 @@ impl Sql {
self.with_conn(|conn| conn.execute(sql, params).map_err(Into::into))
}
pub fn with_conn<T, G>(&self, g: G) -> Result<T>
fn with_conn<T, G>(&self, g: G) -> Result<T>
where
G: FnOnce(&mut Connection) -> Result<T>,
G: FnOnce(&Connection) -> Result<T>,
{
let res = match &*self.pool.read().unwrap() {
Some(pool) => {
let mut conn = pool.get()?;
let conn = pool.get()?;
// Only one process can make changes to the database at one time.
// busy_timeout defines, that if a second process wants write access,
// busy_timeout defines, that if a seconds process wants write access,
// this second process will wait some milliseconds
// and try over until it gets write access or the given timeout is elapsed.
// If the second process does not get write access within the given timeout,
@@ -130,7 +130,7 @@ impl Sql {
// (without a busy_timeout, sqlite3_step() would return SQLITE_BUSY _at once_)
conn.busy_timeout(Duration::from_secs(10))?;
g(&mut conn)
g(&conn)
}
None => Err(Error::SqlNoConnection),
};
@@ -217,37 +217,20 @@ impl Sql {
.unwrap_or_default()
}
/// Executes a query which is expected to return one row and one
/// column. If the query does not return a value or returns SQL
/// `NULL`, returns `Ok(None)`.
pub fn query_get_value_result<P, T>(&self, query: &str, params: P) -> Result<Option<T>>
where
P: IntoIterator,
P::Item: rusqlite::ToSql,
T: rusqlite::types::FromSql,
{
match self.query_row(query, params, |row| row.get::<_, T>(0)) {
Ok(res) => Ok(Some(res)),
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
Err(Error::Sql(rusqlite::Error::InvalidColumnType(
_,
_,
rusqlite::types::Type::Null,
))) => Ok(None),
Err(err) => Err(err),
}
}
/// Not resultified version of `query_get_value_result`. Returns
/// `None` on error.
pub fn query_get_value<P, T>(&self, context: &Context, query: &str, params: P) -> Option<T>
where
P: IntoIterator,
P::Item: rusqlite::ToSql,
T: rusqlite::types::FromSql,
{
match self.query_get_value_result(query, params) {
Ok(res) => res,
match self.query_row(query, params, |row| row.get::<_, T>(0)) {
Ok(res) => Some(res),
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => None,
Err(Error::Sql(rusqlite::Error::InvalidColumnType(
_,
_,
rusqlite::types::Type::Null,
))) => None,
Err(err) => {
error!(context, "sql: Failed query_row: {}", err);
None
@@ -360,7 +343,7 @@ impl Sql {
.and_then(|r| r.parse().ok())
}
pub fn start_stmt(&self, stmt: impl AsRef<str>) {
fn start_stmt(&self, stmt: impl AsRef<str>) {
if let Some(query) = self.in_use.get_cloned() {
let bt = backtrace::Backtrace::new();
eprintln!("old query: {}", query);
@@ -885,19 +868,6 @@ fn open(
update_icons = true;
sql.set_raw_config_int(context, "dbversion", 61)?;
}
if dbversion < 62 {
info!(context, "[migration] v62");
sql.execute(
"ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;",
NO_PARAMS,
)?;
sql.set_raw_config_int(context, "dbversion", 62)?;
}
if dbversion < 63 {
info!(context, "[migration] v63");
sql.execute("UPDATE chats SET grpid='' WHERE type=100", NO_PARAMS)?;
sql.set_raw_config_int(context, "dbversion", 63)?;
}
// (2) updates that require high-level objects
// (the structure is complete now and all objects are usable)

View File

@@ -2,20 +2,22 @@
//!
//! This module is only compiled for test runs.
use std::sync::Arc;
use tempfile::{tempdir, TempDir};
use crate::config::Config;
use crate::constants::KeyType;
use crate::context::{Context, ContextCallback};
use crate::dc_tools::EmailAddress;
use crate::events::Event;
use crate::key::{self, DcKey};
use crate::key;
/// A Context and temporary directory.
///
/// The temporary directory can be used to store the SQLite database,
/// see e.g. [test_context] which does this.
pub(crate) struct TestContext {
pub ctx: Context,
pub struct TestContext {
pub ctx: Arc<Context>,
pub dir: TempDir,
}
@@ -25,14 +27,20 @@ pub(crate) struct TestContext {
/// "db.sqlite" in the [TestContext.dir] directory.
///
/// [Context]: crate::context::Context
pub(crate) fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext {
pub fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let cb: Box<ContextCallback> = match callback {
Some(cb) => cb,
None => Box::new(|_, _| ()),
};
let ctx = Context::new(cb, "FakeOs".into(), dbfile).unwrap();
let ctx = Arc::new(Context::new("FakeOs".into(), dbfile).unwrap());
let ctx_1 = ctx.clone();
std::thread::spawn(move || {
ctx_1.run(|context, event| (*cb)(context, event));
});
TestContext { ctx, dir }
}
@@ -41,11 +49,11 @@ pub(crate) fn test_context(callback: Option<Box<ContextCallback>>) -> TestContex
/// The context will be opened and use the SQLite database as
/// specified in [test_context] but there is no callback hooked up,
/// i.e. [Context::call_cb] will always return `0`.
pub(crate) fn dummy_context() -> TestContext {
pub fn dummy_context() -> TestContext {
test_context(None)
}
pub(crate) fn logging_cb(_ctx: &Context, evt: Event) {
pub fn logging_cb(_ctx: &Context, evt: Event) {
match evt {
Event::Info(msg) => println!("I: {}", msg),
Event::Warning(msg) => println!("W: {}", msg),
@@ -54,50 +62,27 @@ pub(crate) fn logging_cb(_ctx: &Context, evt: Event) {
}
}
/// Load a pre-generated keypair for alice@example.com from disk.
///
/// This saves CPU cycles by avoiding having to generate a key.
///
/// The keypair was created using the crate::key::tests::gen_key test.
pub(crate) fn alice_keypair() -> key::KeyPair {
let addr = EmailAddress::new("alice@example.com").unwrap();
let public =
key::SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
.unwrap();
let secret =
key::SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
.unwrap();
key::KeyPair {
addr,
public,
secret,
}
}
/// Creates Alice with a pre-generated keypair.
///
/// Returns the address of the keypair created (alice@example.com).
pub(crate) fn configure_alice_keypair(ctx: &Context) -> String {
let keypair = alice_keypair();
ctx.set_config(Config::ConfiguredAddr, Some(&keypair.addr.to_string()))
.unwrap();
key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default)
.expect("Failed to save Alice's key");
keypair.addr.to_string()
}
/// Returns the address of the keypair created (alice@example.org).
pub fn configure_alice_keypair(ctx: &Context) -> String {
let addr = String::from("alice@example.org");
ctx.set_config(Config::ConfiguredAddr, Some(&addr)).unwrap();
/// Load a pre-generated keypair for bob@example.net from disk.
///
/// Like [alice_keypair] but a different key and identity.
pub(crate) fn bob_keypair() -> key::KeyPair {
let addr = EmailAddress::new("bob@example.net").unwrap();
// The keypair was created using:
// let (public, private) = crate::pgp::dc_pgp_create_keypair("alice@example.com")
// .unwrap();
// println!("{}", public.to_base64(64));
// println!("{}", private.to_base64(64));
let public =
key::SignedPublicKey::from_base64(include_str!("../test-data/key/bob-public.asc")).unwrap();
let secret =
key::SignedSecretKey::from_base64(include_str!("../test-data/key/bob-secret.asc")).unwrap();
key::KeyPair {
addr,
public,
secret,
}
key::Key::from_base64(include_str!("../test-data/key/public.asc"), KeyType::Public)
.unwrap();
let private = key::Key::from_base64(
include_str!("../test-data/key/private.asc"),
KeyType::Private,
)
.unwrap();
let saved = key::dc_key_save_self_keypair(&ctx, &public, &private, &addr, true, &ctx.sql);
assert_eq!(saved, true, "Failed to save Alice's key");
addr
}

View File

@@ -1 +0,0 @@
mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4MCyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDrRuI8A/8tEEXAA7g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeIeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg==

View File

@@ -1 +0,0 @@
lFgEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5AAAQDMpCY4sD5/DUR0jRjGC5WstwShz1q+5Vofo5mY9+XRXRA3tBlBbGljZSA8YWxpY2VAZXhhbXBsZS5vcmc+iJAEExYIADgWIQQub6LLI7Uy1yhjS1hksI9hqe2UQwUCXlh13QIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBksI9hqe2UQxN6AP4uDAsgzE7I1qg2aEMF/wPxqEztv1dMCj4YyScv/JtqTAD5AYzpxjrcdkmaULyRk2vYfNOEWOog60biPAP/LRBFwAOcXQReWHXdEgorBgEEAZdVAQUBAQdABu3I1stkhQFPCp5bZbm1Vuu6xYsn6dNSa0Xul6stth0DAQgHAAD/X9y9I/JFBeArkgR3U363cWXXxMCWftS+BDwM9zE4PrgQb4h4BBgWCAAgFiEELm+iyyO1MtcoY0tYZLCPYantlEMFAl5Ydd0CGwwACgkQZLCPYantlEMujwEA5sTwaewZXArM2oK8d5aAmyqGNLcLqC9KVXe0Sb1eYXoBANe5wjJV+gHCjIyHaHpxKf8BOflAlvfmmCj6K+neOwwC

View File

@@ -1 +0,0 @@
xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA5//PjAzbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOeJw9kohATSqUtsRO0pFJeDvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeTxc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1JzdKTcDWryrSkvmgFdUqJ7pJDk1HFTt+x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBame1BPsE1PA7VzeTSJR2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS5uZXQ+wsCJBBABCAAzAhkBBQJeMMduAhsDBAsJCAcGFQgJCgsCAxYCARYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSH9oIIALbpmicuVghM3CloiCgJhPEFLFMaQZRDV/KCVVtBcHAhw6d42q8T50mhs+W3Va5E37DN+wcenj8CgeGPQY3kPp2cnZruYtLhLkZ1+VEay5BQFUMb8kY21XrNTQQET8vc0L8cCLQ7RCgm1tGiFVp1nqbjmGUdoru90ksoufWfoqVPjNrW+9eHFvY/Z7PqchCdMnbKOJiwwv4E3NDTySZ1UVZnDztGy95Aa8OZ3cntvbq4JVi7S+N38rRPPPzpZKx+M4DUGfDAoaq7O/Xemyk1sP6C/NgQvS8rri54PgkMgKSS4TyyEzdM+fzeNYFPXFGTbgj4p0pSueQV7/JUfYHRfe3OwE0EXjDHVwEIAKIHgS2yI2niSCN1tqcbLvkhLrEJCVcpGxmA7asl1flwWYrGOBhNJE2sCuZqkofqw6qrgsQ4GFgUU5xmcBCqIZ49jRu+aY38lT4WDFHSbe/mGtaIhb2ZYK6zo9W7Y3r6ud8hbUKJTDfl9qEvJpX/Y0syMjwng8SZNTdYMWgAE4NwcgMgdU3dMA3RT6ePJ4vKs38hmXmInLyZce+GJzmo2tpZyP8viPS7JpqojoCPB3G5h9aHeakp1Y4XKQaExANeWCyBJEhNwtNEOVEpQ0txFYPyDrtxV5y5e79IUP418r/PHsnH6UnxXGzB6LfVbSeEyDyKEl+w0PrlNklySomTZFUAEQEAAcLAdgQYAQgAIAUCXjDHbgIbDBYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSHVCIIAIH694HkQLXRAJlXmi8K/xMVP96ywJovL/B5l4S/vk/iR4P1lYsF55A3Z2PK/iFtwAgVsppcBIPBlqSI0GPDMvEIxj7UFOQfQzVpDes29wG8grHJEJqI/4TlRjOacxTGaJ5fIMsLXJD6nLBuoN5Z6zm3LjqIyOx4ZGrwradPO95OMGT2Xll3YNzUqSWe33RJLqNQ5ea9I7+qvKnW5Z9Yt5nQwOo8yD+f5fql8904B3eAyLqxgkdLmngAWmYhc7KOaKdAsx7TXBAKsoeHk51OPk59u7EbX35HWD6snl/phJdUYDXiddyYN/n2ZY9g80ycle2JfgpfrQGlh7oJqgCjZuk=

View File

@@ -1 +0,0 @@
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

View File

@@ -1 +0,0 @@
xsBNBF48nc4BCADSWr9fE2K1FcE7eSW/Z0MOuzdozKmQJsrmkb7Abssd1yARZuf3+YYh5WqeKJrSTUFD+mJNUhqtodBqBxFH+JzITMG5qGcAdBwXaeJYFMquezLAuVZsZ2b+Njk9Fw3XF4cUZB58ItO/ViFgDi4r5lqsdiiMvGrLmQD2m2BOB8U52ibFhnSVvvYi6rlsZ1HfqB+efD8InPKfiMKDqu909fgVchGJ7OwtTKanaF3v+rzQvXsnVal1yfc0YsmOM0ekWDhIR5EoSg8pJlBHVc/yBrxQWq9h2e2PUntLK29/qp/k/xsQHN3BAj/kuQQPzMARcUUvxo9Aq8n4CMzoFmrK2G+bABEBAAHNFTxjaGFybGllQGV4YW1wbGUubmV0PsLAiQQQAQgAMwIZAQUCXjyd5AIbAwQLCQgHBhUICQoLAgMWAgEWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV7/tMCACSeaPalMt/EwsCGxnWNNEWPX7fMSiAZx1bbPYULIroEBVgmObOkDqoB6y9tWQB/HUq44ZIYdhdw7wlzZp9d81dtxGA+QcRkTy8fr/P2KmQhwjV9m22xBCGABPSpFG+/iONEJADEA8Oe3SogI/iPksepLs9Gg9Ix9Qb1ZMt6+GErE7u0aAamW03NQW7SlgLruAKhjKLjP1wxryK4h0Mdac6CuGZQH/G3yZX3OQPkE2BGjEefhzj0yEKOGhYM716uswqecjB8HoYh5RaDcE+Shcze/OhSQesqj0RxoCpeN3/6QGcC6DT70Qrqeri0RtA495SS3YTqb2p9U2WISOEJIdXzsBNBF48nc4BCADKwwUPt5jp4yIdVdVdLyXGuJS/pC6t5Q64QlcEzHH03eVJHYH42shyWPiTE7HU3giqLhnPXjYN8/piONBQ7dQqTkYJtrx0aUBAoQ9p0p6eMGoY4pMxrclwxnvGE+2YplFNzXkcqxkSPy4n5kOPeq55tVHkWG+gTblZdlT9sKM8Ksr8gF728eaWgt+TQCnhuwa9h9BVzLAZquaATm7PvPKamMd7jIVoISXrZKuPBVrSDbx+DElg0HKj8sOxh3lNz2rmRTGWDbURPhqvVyQ2tXryEVfMZRauR9B9YCYEeedRwDrOSME2LaoU483w0j2Vnb4GkOLpg8Wrw+fnIcM32GXxABEBAAHCwHYEGAEIACAFAl48neQCGwwWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV78HXCACyxjrXpr7GKqQOMJGvNKytuVf7gHUbwLq7oAXoM6PixPkfZIArH/EHnt0GaBWq2r08REj2IRQ8t/8zfkWa9L2RxITez3dRkOUjf49i+J9g3oyleJDZVrAhoU2Uzwb+40tTGaErGpD6m3GqIe5wE7gXFBAf0IwQmIjic64ULCE5j2qaWMxwfvKIDuSD/bN+mSqlQbmekeX6bud4rMoGIUnkQthNAQwBQHOjkPZvdjXiDGFxmpDlcJv5e9LYv8kb141JmYIon9iIGWF3L52+SbvYXFAoPi4qovsdgUXezTRoaiR8Mgft4KUZZIeXUhC7PRuut8PsbN2P0WCh1CjIJph9

View File

@@ -1 +0,0 @@
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

View File

@@ -1 +0,0 @@
xsBNBF48no8BCADwN4PxXewE4iP3HHn/FE5r0x8x8byo9mJIIEOrjL1k0JWIcz3KvC5evfZ3M/ZK2QPBLhFYOck0+gCAR/eH1zgDZeYXUB1CWUoCKlZ9jH1UbfzE34ZxxiwCZy2ZdMnKxDF/ezyXsVvoEhGQE1+8A9Xy/ZXlplUbyYVacgdK9fQlh50d8FOU+7eplUts/SzXx52T83tB98oS9QVgJ4qIC+fu0xMgqdH7e0ithSc/owfYkKjH+isbD4n9U8KSqb4zzf2Y2gz7h6jdM+PPuWqTSARbH83j7qG8q4IjC7cO3CgSryk3f/MLkquKD8z9ybvrYKXSF04RMEuhgBxXnE0vA3ZpABEBAAHNETxkb21AZXhhbXBsZS5uZXQ+wsCJBBABCAAzAhkBBQJePJ6rAhsDBAsJCAcGFQgJCgsCAxYCARYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaoXGQIAM6KRSMBLKOb0IlXqSCy1Ve9MF02fDNmGFw7xC5v2gKpdDZZJctRLxH3ZFgQmpcSIZ4P/A7XcYnlIxxGxwzyhMmas9eJ1w3sPbvX5vJ4ZYGzooZSFUQXgO6+bm7qQlf7gFcsuTkF5n6cx9tDPcyxy01b7eaexTXoWpYDX110TFbpklprub2QpI2w4gy5y1d/pfOmJYGHgMEALgAEKQCWjQ/jUS6pOVEH+pT1ddlv9vq37CmTp00bKbj+Z59mqeUBjE5+RmNWaLwkdZSR1o+LKNw/Gl07gzayRjJyh0Txj4LgHtVSKvGj6GexhddEgqp8qwMMfNbbqWATh9zHNJ+gjSzOwE0EXjyejwEIAKbISj1O986suBXKzHPlEzqetkGEWNV4OHHfhphIxsoW/f3IOdqv0uqwz9AhzXE6YbsFzrc6ZwP+pYTH51T1ugtG0LBMFikh4tyyYjJV0v9gUq2JqixxHfJ/XfVryKO1OWqs2GtBxaQP5FVf+vQqSPpAz4B/6hqwUvx2XdI6bbGn1fQlTQyfGR6Uclm5kIaY8/VBzuVySaAycfOdBhjoyYhl2eMxdxz4NCb6pwNkl6KbjSfGHVpfR6fbuTihIbZFL1qAfYi6BnvrB9HgnfYItIA9VV2SlLIRfBgxgG9vxKmD83j9r5Dn/e1KC5PoMndMZ5I410H06kjR6dLJbTuy7+MAEQEAAcLAdgQYAQgAIAUCXjyeqwIbDBYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaon5UIAMJLU7EKjCsJ4ldIYYRYE7wzVO2DpiFY5yEVu/Jq2O2C+go7b2oBgUYYB3ExwMUwPnU8H6j+jbzukxOltKeRZ1QU1d9iwzYVHsP3kw02DceoyMaab0j1DLHPSVPSmsP5/U0eFK++vFvsY8KuwRUPLK+m7+qFET2gidyRSJGqyiJlEz+wyR3b44Ff1C3RDdRx8EzPVYx+gXRGLIBqsBNuSLhimiwAekMgnpAdsQu8WTCPQSO2Yrz6I6uFVNAr8FnnFoeclwLHbQ++aWhBTuhUp9oJ/388ySkEuMAZrd4YU/RwthM4zsW/VAf936FBkvi8FBJQ0Vdps1xksQeXK7hQrVw=

View File

@@ -1 +0,0 @@
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

View File

@@ -1 +0,0 @@
xsBNBF48nxEBCADRa0HRyoj7KdbchkVycHq5jYxYN4NJsRf1bojhuifuLYlJQu7I7Rp862sbvSPN9tM/3dQF6fTyUllFkhoS50fUaf/bJWwi5XWVsLa1+DdOW4As2zkJslu6Hp/hUFM2FXthRYIwg3c2jparkNtsLyQvtDsG62Acg7dr+NwZ5mxepyJ5WkXciOPLp9egcrTVoBbcnn4gzj2Wx6MkUByY+bENtUrqsZerWOt+F75DIWkHwzo5VwfpvH6RrIJabu8KLMLoUTZoovQcDKfk0yv8bdBLEQf84SLY+0BCZ6aZHrqnwEmDBeFGfTcIMFsBxUZfvsMYlAiG6khbZzhQxJ44+YaxABEBAAHNEzxlbGVuYUBleGFtcGxlLm5ldD7CwIkEEAEIADMCGQEFAl48ny0CGwMECwkIBwYVCAkKCwIDFgIBFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noIpsAgAiP+E48xEhCvEHVIMiQigQvC3kucg2TRk0yrp0ydDeS19l8jXkN8lA9byXVq5VDg8Bs4tN9WR/Gy8x9Xmd6/VyFiquBqGTObO/1u1kegHR0+sp5FxxffZhGoCRBIW4iXww1BzcGUd+TeRlInl5adGpA6vQAGifLsKQQU9/I0bSRBAMGbo0PKZ+EllTLqDeo4D4ELNO6CKyh65FExUIZTtbWZTyLn95ekypUIhgEAKgEs/qE74nJBqcrzkXn3xmDJetYxw2xQzwS+rIwR12ONub01nm/74Z30zbMViwQF4yTV7VB9uklY6WayCBpT3BkFQiYRPt7lnipBI2PplYXvYIs7ATQRePJ8RAQgAwvvNWB4eABzpUylyhI7q8WNK8GHCGLfqprSLFiAv14psluRW9MezLEFM1N8Az5yqzs3hsuEMiIBPiLrkW8ZkCledlYENorM/6G5+xK9TI4iWnP1LP+qESGGNSF7pXciZE7/XrE/CPL06nXuJRd1qOHvIUnaCQRiqLFPkUG3KhtC+/Ayvz6l2RvSqoTTayxYckgigkEneS/RXMSYnG/A5RJB0fOACp39HzHN/XU7JFLz8WlXgLfWJi7v9uVft9QBCtVseWF+ElKJ5NkVNRrV/SQwFYB91Y+LHqKz6CTQq2Di0vGHjY9ecai16D/CGUqkJg9t5jnLxZvQeR70o13JqlQARAQABwsB2BBgBCAAgBQJePJ8uAhsMFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noLj2gf9EuLnhKUv0rWW2HShJlKF5oqKmx6tSoNMOd5hbJzL1Z9d7ZjFQJS25jAzl9V6858heAm3HYTlJfg2RkzHte12k0hr9Svbpadf/OYB2UVsAwGRFGbdq/3H/ZaP5EbCdv8Egcx6pjdgvXb5n6gaMj7LJG4/YvokGLOx3VgNNRdqB0gtKWwN0tzdM/6YfDJTEVDbMUR3aFqYLTSB8KCjNtWcnWO2a17P1Qf6fDhcxpyELkLb1T4sPD4Tz9bFPTJzL87uS4+Ba+Xq2YE7aWa6e+C2HMgH6OghDLqoGpeNy01N3XSikyoFNdEmPYY/RdmuJNSuuyptQHgzh1vIOV7AOoQ+Rw==

View File

@@ -1 +0,0 @@
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

View File

@@ -1 +0,0 @@
xsBNBF48n2MBCAC741/YKzU76pWNijBCYC+hGSKZvBdanm4eJzi/eKPevUypdW2GJzFFJxWjx2wIcV8cLwQvY2Z+ieJaPPwGbUr0nH2S9lmghkCCKjGxWcmrx3sQr7x9431KYOM6+/SAZNjHYjWwcwy2QhE6J7qfC74LjTF4pv+ZRgSZDC+O70NpTWdNdwi/0Kn9gdj5diSwJmnBxQBGp/tnZuu3XGZrgKXlGlPspRMF3Ug2WmvyBhxF8KKoosiaXlumc5gFFuFRnoAVfp/6yehO44l9bYz0zwYWahxmLhShQVhfcH3YBg0l7hMkfC2Fp8fDF7Hr1PUOJcBY50VDN0zlV9Bi1iiPofAtABEBAAHNEzxmaW9uYUBleGFtcGxlLm5ldD7CwIkEEAEIADMCGQEFAl48n3wCGwMECwkIBwYVCAkKCwIDFgIBFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZUGWQgApCPQYmfa76xcscYBWg3DvMAm0Exk/LvbN74MIqADHIgaNFUHoThTPDVAPeh5ogra/kg4QaLctivC81S2VOPIc/4LGEFJekvythXUgSLNjhlR63Va5ObMV4UegUH9c0O8MGndkQFxiwy98D1bzNyl3mCVOUECVZiJJbxUZwBKj4iowa7kgX/FrdjZIJrz50NMExcDuxvc+MGyDr4YQHrZTbCjcrJkxufklwsWme0qneuPTxwW2+TnDjHkDaYDdwEnoKRkwlXEy3Lu9ZhswlaWTgdtClpf2geb7tCAPQ0cd2V5m6NnKMzXrnFuJXB6Xkvp32Tyuq4ebILsbCfpLV/HzM7ATQRePJ9jAQgAzQGckmcFjCViSkwGLROhi2Gvb85ABXbkKPMg1x27LraP9YWNJoq2GOy2vv8r8m2q+FpCR25a0NdWlbyiQpPuEWh0udJnNUm+6j5j6PSAmlRRDMhDH4QwzJC+B+lM6NxvSRhxBBtwFAvMy0qeNhv1UCBaeQUHoFCODqJRYOywj7ZGXdQyttIAlC5PtVw1cV+J8/TGXNrE9DNgFGAm1BPgw/lE1OjVbF8l6NbDdi9YJoOm9mpffUPlUZMZh3+a5+J6F3KjYwNyi4wi3Y3Pt4avXEo+ib15XNhJcwMslc49La2T8PMnpf3nLClxynJjYDiMuzujcbBWbfVF1383P0KikQARAQABwsB2BBgBCAAgBQJePJ98AhsMFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZX1eAf9GnrA9nCds3OmtCUpRmVEDar29DfS79B4vBfoYXYb5kRIOxJV6yjfGYRh2IJ0CTfNYkp4AuRC/jEHPXlVUD92Vcb0wSfwO32mMw75FRzIS7/IcwWAauWOtpao3J/tsxHWkSxfh5Zo6vzODQY4k8eHnitxpXT0xSIjnYmVRMuqfb3NELk3PkF52+Fte4zKfBCP80HqO3BdVCBlQNpZiOMW+yFO/8VqLmB9442nGGhuSfCeBystI3Er0SYSDqNbg5uetBaP1MOomIZuuxhv3T5jD9DeEDHGOqDjPZ8dMdQYCnYmiVaMb8ECsKG5eMvS8Imss1ho2iz4sjYdVzODl8T0zQ==

View File

@@ -1 +0,0 @@
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

View File

@@ -0,0 +1 @@
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

1
test-data/key/public.asc Normal file
View File

@@ -0,0 +1 @@
xsBNBF086ewBCACmJKuoxIO6T87yi4Q3MyNpMch3Y8KrtHDQyUszU36eqM3Pmd1lFrbcCd8ZWo2pq6OJSwsM/jjRGn1zo2YOaQeJRRrC+KrKGqSUtRSYQBPrPjE2YjSXAMbu8jBI6VVUhHeghriBkK79PY9O/oRhIJC0p14IJe6CQ6OI2fTmTUHF9i/nJ3G4Wb3/K1bU3yVfyPZjTZQPYPvvh03vxHULKurtYkX5DTEMSWsF4qzLMps+l87VuLV9iQnbN7YMRLHHx2KkX5Ivi7JCefhCa54M0K3bDCCxuVWAM5wjQwNZjzR+WL+dYchwoFvuF8NrlzjM9dSv+2rn+J7f99ijSXarzPC7ABEBAAHNEzxhbGljZUBleGFtcGxlLmNvbT7CwIkEEAEIADMCGQEFAl086fgCGwMECwkIBwYVCAkKCwIDFgIBFiEE+iaix4d0doj87Q0ek6DcNkbrcegACgkQk6DcNkbrcei3ogf/cruUmQ+th52TFHTHdkw9OHUl3MrXtZ7QmHyOAFvbXE/6n5Eeh+eZoF8MWWV72m14Wbs+vTcNQkFVTdOPptkKA8e4cJqwDOHsyAnvQXZ7WNje9+BMzcoipIUawHP4ORFaPDsKLZQ0b4wBbKn8ziea6zjGF0/qljTdoxTtsYpv5wXYuhwbYklrLOqgSa5M7LXUe7E3g9mbg+9iX1GuB8m6GkquJN814Y+xny4xhZzGOfue6SeP12jJMNSjSP7416dRq7794VGnkkW9V/7oFEUKu5wO9FFbgDySOSlEjByGejSGuBmho0iJSjcPjZ7EY/j3M3orq4dpza5C82OeSvxDFc7ATQRdPOnsAQgA5oLxXRLnyugzOmNCy7dxV3JrDZscA6JNlJlDWIShT0YSs+zG9JzDeQql+sYXgUSxOoIayItuXtnFn7tstwGoOnYvadm/e5/7V5fKAQRtCtdN51av62n18Venlm0yNKpROPcZ6M/sc4m6uU6YRZ/a1idal8VGY0wLKlghjIBuIiBoVQ/RnoW+/fhmwIg08dQ5m8hQe3GEOZEeLrTWL/9awPlXK7Y+DmJOoR4qbHWEcRfbzS6q4zW8vk2ztB8ngwbnqYy8zrN1DCICN1gYdlU++uVw6Bb1XfY8Cdldh1VLKpF35mAmjxLZfVDcoObFH3Cv2GB7BEYxv86KC2Y6T74Q/wARAQABwsB2BBgBCAAgBQJdPOn4AhsMFiEE+iaix4d0doj87Q0ek6DcNkbrcegACgkQk6DcNkbrcegLxwf/dXshJnoWqEnRsf6rVb9/Mc66ti+NVQLfNd275dybh/QIJdK3NmSxdnTPIEJRVspJywJoupwXFNrnHG2Ff6QPvHqI+/oNu986r9d7Z1oQibbLHKt8t6kpOfg/xGxotagJuiCQvR9mMjD1DqsdB37SjDxGupZOOJSXWi6KX60IE+uM+QOBfeOZziQwuFmA5wV6RDXIeYJfqrcbeXeR1d0nfNpPHQR1gBiqmxNb6KBbdXD2+EXW60axC7D2b1APmzMlammDliPwsK9U1rc9nuquEBvGDOJf4K+Dzn+mDCqRpP6uAuQ7RKHyim4uyN0wwKObzPqgJCGwjTglkixw+aSTXw==

View File

@@ -1,7 +1,12 @@
//! Stress some functions for testing; if used as a lib, this file is obsolete.
use std::collections::HashSet;
use std::sync::Arc;
use deltachat::config;
use deltachat::context::*;
use deltachat::keyring::*;
use deltachat::pgp;
use deltachat::Event;
use tempfile::{tempdir, TempDir};
@@ -90,18 +95,133 @@ fn stress_functions(context: &Context) {
// free(qr.cast());
}
#[test]
#[ignore] // is too expensive
fn test_encryption_decryption() {
let (public_key, private_key) = pgp::create_keypair("foo@bar.de").unwrap();
private_key.split_key().unwrap();
let (public_key2, private_key2) = pgp::create_keypair("two@zwo.de").unwrap();
assert_ne!(public_key, public_key2);
let original_text = b"This is a test";
let mut keyring = Keyring::default();
keyring.add_owned(public_key.clone());
keyring.add_ref(&public_key2);
let ctext_signed = pgp::pk_encrypt(original_text, &keyring, Some(&private_key)).unwrap();
assert!(!ctext_signed.is_empty());
assert!(ctext_signed.starts_with("-----BEGIN PGP MESSAGE-----"));
let ctext_unsigned = pgp::pk_encrypt(original_text, &keyring, None).unwrap();
assert!(!ctext_unsigned.is_empty());
assert!(ctext_unsigned.starts_with("-----BEGIN PGP MESSAGE-----"));
let mut keyring = Keyring::default();
keyring.add_owned(private_key);
let mut public_keyring = Keyring::default();
public_keyring.add_ref(&public_key);
let mut public_keyring2 = Keyring::default();
public_keyring2.add_owned(public_key2);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text,);
assert_eq!(valid_signatures.len(), 1);
valid_signatures.clear();
let empty_keyring = Keyring::default();
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&empty_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 0);
valid_signatures.clear();
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring2,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 0);
valid_signatures.clear();
public_keyring2.add_ref(&public_key);
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring2,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 1);
valid_signatures.clear();
let plain = pgp::pk_decrypt(
ctext_unsigned.as_bytes(),
&keyring,
&public_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
valid_signatures.clear();
let mut keyring = Keyring::default();
keyring.add_ref(&private_key2);
let mut public_keyring = Keyring::default();
public_keyring.add_ref(&public_key);
let plain = pgp::pk_decrypt(ctext_signed.as_bytes(), &keyring, &public_keyring, None).unwrap();
assert_eq!(plain, original_text);
}
fn cb(_context: &Context, _event: Event) {}
#[allow(dead_code)]
struct TestContext {
ctx: Context,
ctx: Arc<Context>,
dir: TempDir,
}
fn create_test_context() -> TestContext {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let ctx = Context::new(Box::new(cb), "FakeOs".into(), dbfile).unwrap();
let ctx = Arc::new(Context::new("FakeOs".into(), dbfile).unwrap());
let ctx_1 = ctx.clone();
std::thread::spawn(move || {
ctx_1.run(cb);
});
TestContext { ctx, dir }
}