Compare commits

..

27 Commits

Author SHA1 Message Date
Floris Bruynooghe
ce5c42e11e Use anyhow's with_context 2022-02-07 21:41:55 +01:00
Floris Bruynooghe
a0df000fb1 small typo 2022-02-07 21:25:49 +01:00
Floris Bruynooghe
8bd58a0d51 Return selected account by ID
Otherwise you do not know which account needs unlocking if the
selected account is locked.
2022-02-07 21:19:48 +01:00
Floris Bruynooghe
2d14f1e187 Fixup naming a bit
Originally I was calling thing "password" a lot, but the consistent
naming is "passphrase" and "encrypted account".
2022-02-07 20:50:50 +01:00
Floris Bruynooghe
cd6aba1e57 Missing rustfmt 2022-02-06 22:12:29 +01:00
Floris Bruynooghe
287e291485 Fixup doc links 2022-02-06 22:11:54 +01:00
Floris Bruynooghe
926e3208ef Add changelog 2022-02-06 22:10:01 +01:00
Floris Bruynooghe
059dd54b0e Remove now usused function 2022-02-06 22:07:30 +01:00
Floris Bruynooghe
8bc2ca7f25 Make working with encrypted storage RAII
This refactors the APIs to work with encrypted storage to folow the
Resource Acquisition Is Initialisation principle.  Having our
structures behave like this is beneficial because it reoves a lot of
edge-cases that would need to be handled.
2022-02-06 21:38:17 +01:00
link2xt
2e5e8f73c6 imap: simplify get_quota_roots() 2022-02-06 15:17:05 +00:00
link2xt
ada5d38272 imap: remove unwrap() 2022-02-06 14:07:04 +00:00
link2xt
c4b0f773db python: remove arbitrary timeouts from tests
pytest-timeout already handles all deadlocks and is configurable with
--timeout option. With this change it is possible to disable timeout
with --timeout 0 to run tests on extremely slow connections.
2022-02-06 12:52:48 +00:00
link2xt
276daf631e imap: move messages in batches
Also change how NO response is treated. NO response means there is an
error moving/copying the messages. When there are no matching
messages, the response is "OK No matching messages, so nothing copied"
according to some RFC 9051 examples.
2022-02-05 22:15:46 +00:00
link2xt
fb19b58147 Reduce number of unsafe as conversions
Enable clippy::cast_lossless lint and get rid of
some conversions pointed out by  clippy::as_conversions.
2022-02-05 12:42:14 +00:00
dependabot[bot]
13a5e3cf6f Merge pull request #3055 from deltachat/dependabot/cargo/async-std-resolver-0.20.4 2022-02-04 21:39:59 +00:00
bjoern
1caf3caf1b do set_visibility() in a transaction (#3053)
this avoids archived chats containing fresh messages:

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

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

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

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

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

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

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

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

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

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

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

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

* Add OnlyFetchMvbox option

* Add test for the config

* Add option to only watch mvbox

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

* Fixup ignore conditions

* Cleanup some bits

* Watch the mvbox when `WatchMvboxOnly` is set

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

* typo

* clippy, more typos

Co-authored-by: Hocuri <hocuri@gmx.de>
2022-01-31 13:39:48 +01:00
link2xt
f304a30193 imap: fetch Inbox before scanning other folders 2022-01-31 12:03:21 +01:00
link2xt
7eadca3959 imap: do not synchronize Seen flags on unwatched folders
Synchronizing seen flags doubles the time required to scan all
folders. Delta Chat only marks messages as Seen on Inbox or DeltaChat,
so there is no need to check for Seen flag on other folders.
2022-01-30 20:00:00 +00:00
29 changed files with 923 additions and 661 deletions

View File

@@ -2,20 +2,52 @@
## Unreleased
### Changes
- refactorings #3026
- move messages in batches #3058
- working with encrypted storage drastically changed to be RAII #3063
### Fixes
- avoid archived, fresh chats #3053
- treat "NO" IMAP response to MOVE and COPY commands as an error #3058
## 1.75.0
### Changes
- optimize `delete_expired_imap_messages()` #3047
## 1.74.0
### Fixes
- avoid reconnection loop when message without Message-ID is marked as seen #3044
## 1.73.0
### API changes
- added `only_fetch_mvbox` config #3028
### Changes
- don't watch Sent folder by default #3025
- use webxdc app name in chatlist/quotes/replies etc. #3027
- refactorings #3023
- remove direct dependency on `byteorder` crate #3031
- make it possible to cancel message sending by removing the message #3034,
this was previosuly removed in 1.71.0 #2939
- always skip Seen flag synchronization when there are no updates #3039
- synchronize Seen flags only on watched folders to speed up
folder scanning #3041
- remove direct dependency on `byteorder` crate #3031
- refactorings #3023 #3013
- update provider database #3043
- improve documentation #3017 #3018 #3021
### Fixes
- fix splitting off text from webxdc messages #3032
- call slow `delete_expired_imap_messages()` less often #3037
- make synchronization of Seen status more robust in case unsolicited FETCH
result without UID is returned #3022
- fetch Inbox before scanning folders to ensure iOS does
not kill the app before it gets to fetch the Inbox in background #3040
## 1.72.0

17
Cargo.lock generated
View File

@@ -347,9 +347,9 @@ dependencies = [
[[package]]
name = "async-std-resolver"
version = "0.20.3"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed4e2c3da14d8ad45acb1e3191db7a918e9505b6f155b218e70a7c9a1a48c638"
checksum = "dbf3e776afdf3a2477ef4854b85ba0dff3bd85792f685fb3c68948b4d304e4f0"
dependencies = [
"async-std",
"async-trait",
@@ -1063,7 +1063,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.72.0"
version = "1.75.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1090,7 +1090,6 @@ dependencies = [
"hex",
"humansize",
"image",
"imap-proto",
"kamadak-exif",
"lettre_email",
"libc",
@@ -1144,7 +1143,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.72.0"
version = "1.75.0"
dependencies = [
"anyhow",
"async-std",
@@ -3871,9 +3870,9 @@ dependencies = [
[[package]]
name = "trust-dns-proto"
version = "0.20.3"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0d7f5db438199a6e2609debe3f69f808d074e0a2888ee0bccb45fe234d03f4"
checksum = "ca94d4e9feb6a181c690c4040d7a24ef34018d8313ac5044a61d21222ae24e31"
dependencies = [
"async-trait",
"cfg-if 1.0.0",
@@ -3895,9 +3894,9 @@ dependencies = [
[[package]]
name = "trust-dns-resolver"
version = "0.20.3"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ad17b608a64bd0735e67bde16b0636f8aa8591f831a25d18443ed00a699770"
checksum = "ecae383baad9995efaa34ce8e57d12c3f305e545887472a492b838f4b5cfb77a"
dependencies = [
"cfg-if 1.0.0",
"futures-util",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.72.0"
version = "1.75.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -38,7 +38,6 @@ escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
imap-proto = "0.14.3"
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"

View File

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

View File

@@ -179,10 +179,12 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
// create/open/config/information
/**
* Create a new context object and try to open it without passphrase. If
* database is encrypted, the result is the same as using
* dc_context_new_closed() and the database should be opened with
* dc_context_open() before using.
* Create a new context object and try to open it without passphrase.
*
* If database is encrypted NULL is returned, encrypted databases should be
* opened using dc_context_new_encrypted().
*
* If the database does not yet exist a new one will be created.
*
* @memberof dc_context_t
* @param os_name Deprecated, pass NULL or empty string here.
@@ -192,54 +194,34 @@ typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
* @return A context object with some public members.
* The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage.
* On failure NULL is returned.
*
* If you want to use multiple context objects at the same time,
* this can be managed using dc_accounts_t.
*/
dc_context_t* dc_context_new (const char* os_name, const char* dbfile, const char* blobdir);
/**
* Create a new context object. After creation it is usually opened with
* dc_context_open() and started with dc_start_io() so it is connected and
* mails are fetched.
* Create a new context object and try to open it with a passphrase.
*
* If the database does not yet exist a new one will be created. If the
* database is not encrypted this will fail.
*
* @memberof dc_context_t
* @param dbfile The file to use to store the database,
* something like `~/file` won't work, use absolute paths.
* @param passphrase The passphrase to use. This MUST be non-NULL and MUST be valid
* UTF-8, if either of this is not true this fails and NULL is retruned.
* @return A context object with some public members.
* The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage.
* On failure NULL is returned.
*
* If you want to use multiple context objects at the same time,
* this can be managed using dc_accounts_t.
*/
dc_context_t* dc_context_new_closed (const char* dbfile);
/**
* Opens the database with the given passphrase. This can only be used on
* closed context, such as created by dc_context_new_closed(). If the database
* is new, this operation sets the database passphrase. For existing databases
* the passphrase should be the one used to encrypt the database the first
* time.
*
* @memberof dc_context_t
* @param context The context object.
* @param passphrase The passphrase to use with the database. Pass NULL or
* empty string to use no passphrase and no encryption.
* @return 1 if the database is opened with this passphrase, 0 if the
* passphrase is incorrect and on error.
*/
int dc_context_open (dc_context_t *context, const char* passphrase);
/**
* Returns 1 if database is open.
*
* @memberof dc_context_t
* @param context The context object.
* @return 1 if database is open, 0 if database is closed
*/
int dc_context_is_open (dc_context_t *context);
dc_context_t* dc_context_new_encrypted (const char* dbfile, const char* passphrase);
/**
* Free a context object.
@@ -343,6 +325,12 @@ char* dc_get_blobdir (const dc_context_t* context);
* and watch the `DeltaChat` folder for updates (default),
* 0=do not move chat-messages
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
* `DeltaChat` folder. Messages will still be fetched from the
* spam folder and `sendbox_watch` will also still be respected
* if enabled.
* 0=watch all folders normally (default)
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
* show direct replies to chats only (default),
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
@@ -2631,21 +2619,22 @@ void dc_accounts_unref (dc_accounts_t* accounts);
uint32_t dc_accounts_add_account (dc_accounts_t* accounts);
/**
* Add a new closed account to the account manager.
* Internally, dc_context_new_closed() is called using a unique database-name
* in the directory specified at dc_accounts_new().
* Add a new account to the account manager, encrypted with a passphrase.
*
* If the function succeeds,
* dc_accounts_get_all() will return one more account
* and you can access the newly created account using dc_accounts_get_account().
* Moreover, the newly created account will be the selected one.
* The account's database will be encrypted with a key derived from the
* passphrase and will need to be opened with the passphrase to be usable.
*
* Encrypted accounts are not automatically opened when the account
* manager is created. They need to be opened using dc_accounts_load_encrypted(),
* to find unloaded encrypted accounts use dc_accounts_get_encrypted().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return Account-id, use dc_accounts_get_account() to get the context object.
* @return Account-id, use dc_accounts_get_encrypted_account() to get the
* context object.
* On errors, 0 is returned.
*/
uint32_t dc_accounts_add_closed_account (dc_accounts_t* accounts);
uint32_t dc_accounts_add_encrypted_account(dc_accounts_t* accounts, char* passphrase);
/**
* Migrate independent accounts into accounts managed by the account manager.
@@ -2681,6 +2670,10 @@ int dc_accounts_remove_account (dc_accounts_t* accounts, uint32
/**
* List all accounts.
*
* Only lists loaded accounts, some accounts might be encrypted and need to be
* explicitly loaded using dc_accounts_get_encrypted() and
* dc_accounts_load_encrypted().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return An array containing all account-ids,
@@ -2688,10 +2681,27 @@ int dc_accounts_remove_account (dc_accounts_t* accounts, uint32
*/
dc_array_t* dc_accounts_get_all (dc_accounts_t* accounts);
/**
* Lists encrypted accounts.
*
* This returns all *locked* encrypted accounts which must be activated using
* dc_accounts_load_encrypted(). Once an encrypted account has been loaded it will
* show up in dc_accounts_get_all() and can be retrieved using
* dc_accounts_get_account().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return An array containing account-ids, use dc_array_get_id() to get the ids.
*/
dc_array_t* dc_accounts_get_encrypted (dc_accounts_t* accounts);
/**
* Get an account-context from an account-id.
*
* Only accounts returned by dc_accounts_get_all() can be retrieved with this
* function, encrypted accounts first need to be loaded using
* dc_accounts_load_encrypted().
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @param account_id The account-id as returned e.g. by dc_accounts_get_all() or dc_accounts_add_account().
@@ -2702,9 +2712,30 @@ dc_array_t* dc_accounts_get_all (dc_accounts_t* accounts);
*/
dc_context_t* dc_accounts_get_account (dc_accounts_t* accounts, uint32_t account_id);
/**
* Loads an encrypted account into the Account manager.
*
* Once the account has been loaded you can also access it using
* dc_accounts_get_account() like any other account. Use *dc_accounts_get_encrypted()
* to find accounts which need to be loaded like this.
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @param account_id The account-id as returned e.g. by dc_accounts_get_encrypted(),
* dc_accounts_add_encrypted_account().
* @param passphrase The passphrase to decrypt the account with.
* @return The account-context, this can be used most similar as a normal,
* unmanaged account-context as created by dc_context_new().
* Once you do no longer need the context-object, you have to call dc_context_unref() on it,
* which, however, will not close the account but only decrease a reference counter.
* On failure NULL is returned.
*/
dc_context_t* dc_accounts_load_encrypted (dc_accounts_t* accounts, uint32_t account_id, char* passphrase);
/**
* Get the currently selected account.
* Get the currently selected account ID.
*
* If there is at least one account in the account-manager,
* there is always a selected one.
* To change the selected account, use dc_accounts_select_account();
@@ -2712,13 +2743,15 @@ dc_context_t* dc_accounts_get_account (dc_accounts_t* accounts, uint32
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return The account-context, this can be used most similar as a normal,
* unmanaged account-context as created by dc_context_new().
* Once you do no longer need the context-object, you have to call dc_context_unref() on it,
* which, however, will not close the account but only decrease a reference counter.
* If there is no selected account, NULL is returned.
* @return The account ID of the selected account. The context of
* the selected account can then be retrieved using dc_accounts_get_account().
* Note that if the selected account is encrypted you may first have to
* load it using dc_accounts_load_encrypted(), you can verify if this is needed
* using dc_accounts_get_encrypted().
* If no account is selected 0 is returned. However this is only possible if
* there is no single account in the account manager.
*/
dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
uint32_t* dc_accounts_get_selected_account_id (dc_accounts_t* accounts);
/**

View File

@@ -16,6 +16,7 @@ extern crate serde_json;
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::ffi::CStr;
use std::fmt::Write;
use std::ops::Deref;
use std::ptr;
@@ -94,20 +95,33 @@ pub unsafe extern "C" fn dc_context_new(
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *mut dc_context_t {
pub unsafe extern "C" fn dc_context_new_encrypted(
dbfile: *const libc::c_char,
passphrase: *const libc::c_char,
) -> *mut dc_context_t {
setup_panic!();
if dbfile.is_null() {
eprintln!("ignoring careless call to dc_context_new_closed()");
if dbfile.is_null() || passphrase.is_null() {
eprintln!("ignoring careless call to dc_context_new_encrypted()");
return ptr::null_mut();
}
// Generate random ID if dc_accounts_t is not used.
let id = rand::thread_rng().gen();
match block_on(Context::new_closed(
as_path(dbfile).to_path_buf().into(),
let dbfile = as_path(dbfile).to_path_buf();
let cstr = CStr::from_ptr(passphrase);
let passphrase = match cstr.to_str() {
Ok(s) => s,
Err(err) => {
eprintln!("passphrase was not UTF-8: {:#}", err);
return ptr::null_mut();
}
};
let ctx = block_on(Context::new_encrypted(
dbfile.into(),
id,
)) {
Ok(context) => Box::into_raw(Box::new(context)),
passphrase.to_string(),
));
match ctx {
Ok(ctx) => Box::into_raw(Box::new(ctx)),
Err(err) => {
eprintln!("failed to create context: {:#}", err);
ptr::null_mut()
@@ -115,35 +129,6 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_open(
context: *mut dc_context_t,
passphrase: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_open()");
return 0;
}
let ctx = &*context;
let passphrase = to_string_lossy(passphrase);
block_on(ctx.open(passphrase))
.log_err(ctx, "dc_context_open() failed")
.map(|b| b as libc::c_int)
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_is_open()");
return 0;
}
let ctx = &*context;
block_on(ctx.is_open()) as libc::c_int
}
/// Release the context structure.
///
/// This function releases the memory of the `dc_context_t` structure.
@@ -4083,18 +4068,17 @@ pub unsafe extern "C" fn dc_accounts_get_account(
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_selected_account(
pub unsafe extern "C" fn dc_accounts_get_selected_account_id(
accounts: *mut dc_accounts_t,
) -> *mut dc_context_t {
) -> u32 {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_selected_account()");
return ptr::null_mut();
return 0;
}
let accounts = &*accounts;
block_on(async move { accounts.read().await.get_selected_account().await })
.map(|ctx| Box::into_raw(Box::new(ctx)))
.unwrap_or_else(std::ptr::null_mut)
block_on(async move { accounts.read().await.get_selected_account_id().await })
.unwrap_or(0)
}
#[no_mangle]
@@ -4148,17 +4132,29 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accounts_t) -> u32 {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_add_closed_account()");
pub unsafe extern "C" fn dc_accounts_add_encrypted_account(
accounts: *mut dc_accounts_t,
passphrase: *const libc::c_char,
) -> u32 {
if accounts.is_null() || passphrase.is_null() {
eprintln!("ignoring careless call to dc_accounts_add_with_password()");
return 0;
}
let accounts = &mut *accounts;
let accounts: &AccountsWrapper = &mut *accounts;
let cstr = CStr::from_ptr(passphrase);
block_on(async move {
let mut accounts = accounts.write().await;
match accounts.add_closed_account().await {
let passphrase = match cstr.to_str() {
Ok(s) => s,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Passphrase was not UTF-8: {:#}",
err
)));
return 0;
}
};
match accounts.add_encrypted_account(passphrase.to_string()).await {
Ok(id) => id,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
@@ -4243,6 +4239,60 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_encrypted(
accounts: *mut dc_accounts_t,
) -> *mut dc_array_t {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_all()");
return ptr::null_mut();
}
let accounts: &AccountsWrapper = &*accounts;
let list = block_on(async move { accounts.read().await.get_encrypted() });
let array: dc_array_t = list.into();
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_load_encrypted(
accounts: *mut dc_accounts_t,
account_id: u32,
passphrase: *const libc::c_char,
) -> *mut dc_context_t {
if accounts.is_null() || passphrase.is_null() {
eprintln!("ignoring careless call to dc_context_new_encrypted()");
return ptr::null_mut();
}
let accounts: &AccountsWrapper = &*accounts;
let cstr = CStr::from_ptr(passphrase);
block_on(async move {
let mut accounts = accounts.write().await;
let passphrase = match cstr.to_str() {
Ok(s) => s,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Passphrase was not UTF-8: {:#}",
err
)));
return ptr::null_mut();
}
};
match accounts
.load_encrypted_account(account_id, passphrase.to_string())
.await
{
Ok(ctx) => Box::into_raw(Box::new(ctx)),
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Failed to load encrypted account: {:#}",
err
)));
ptr::null_mut()
}
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_all_work_done(accounts: *mut dc_accounts_t) -> libc::c_int {
if accounts.is_null() {

View File

@@ -224,9 +224,7 @@ class DirectImap:
""" (blocking) wait for next idle message from server. """
assert self._idling
self.account.log("imap-direct: calling idle_check")
res = self.conn.idle_check(timeout=30)
if len(res) == 0:
raise TimeoutError
res = self.conn.idle_check()
if terminate:
self.idle_done()
self.account.log("imap-direct: idle_check returned {!r}".format(res))

View File

@@ -241,7 +241,6 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def make_account(self, path, logid, quiet=False):
ac = Account(path, logging=self._logging)
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
ac._evtracker.set_timeout(30)
ac.addr = ac.get_self_contact().addr
ac.set_config("displayname", logid)
if not quiet:
@@ -483,7 +482,7 @@ class BotProcess:
def kill(self) -> None:
self.popen.kill()
def wait(self, timeout=30) -> None:
def wait(self, timeout=None) -> None:
self.popen.wait(timeout=timeout)
def fnmatch_lines(self, pattern_lines):
@@ -492,7 +491,7 @@ class BotProcess:
print("+++FNMATCH:", next_pattern)
ignored = []
while 1:
line = self.stdout_queue.get(timeout=15)
line = self.stdout_queue.get()
if line is None:
if ignored:
print("BOT stdout terminated after these lines")

View File

@@ -652,8 +652,6 @@ class TestOnlineAccount:
pre_generated_key=False,
config={"key_gen_type": str(const.DC_KEY_GEN_ED25519)}
)
# rsa key gen can be slow especially on CI, adjust timeout
ac1._evtracker.set_timeout(240)
acfactory.wait_configure_and_start_io()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -892,11 +890,11 @@ class TestOnlineAccount:
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message1")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
chat.send_text("message2")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
chat.send_text("message3")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
def test_forward_messages(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1834,7 +1832,6 @@ class TestOnlineAccount:
lp.sec("trigger ac setup message and return setupcode")
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
setup_code = ac1.initiate_key_transfer()
ac2._evtracker.set_timeout(30)
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg.is_setup_message()
@@ -1851,7 +1848,6 @@ class TestOnlineAccount:
def test_ac_setup_message_twice(self, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.clone_online_account(ac1)
ac2._evtracker.set_timeout(30)
acfactory.wait_configure_and_start_io()
lp.sec("trigger ac setup message but ignore")
@@ -2025,7 +2021,7 @@ class TestOnlineAccount:
lp.sec("ac1: send a message to group chat to promote the group")
chat.send_text("afterwards promoted")
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "chat-modified"
assert chat.is_promoted()
assert sorted(x.addr for x in chat.get_contacts()) == \
@@ -2035,29 +2031,29 @@ class TestOnlineAccount:
# note that if the above create_chat() would not
# happen we would not receive a proper member_added event
contact2 = chat.add_contact("devnull@testrun.org")
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "added"
assert ev.message.get_sender_contact().addr == ac1_addr
assert ev.contact.addr == "devnull@testrun.org"
lp.sec("ac1: remove address2")
chat.remove_contact(contact2)
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "removed"
assert ev.contact.addr == contact2.addr
assert ev.message.get_sender_contact().addr == ac1_addr
lp.sec("ac1: remove ac2 contact from chat")
chat.remove_contact(ac2)
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
ev = in_list.get()
assert ev.action == "removed"
assert ev.message.get_sender_contact().addr == ac1_addr
@@ -2682,7 +2678,13 @@ class TestOnlineAccount:
ac1.direct_imap.select_config_folder("inbox")
ac1.direct_imap.idle_start()
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
ac1.direct_imap.idle_check(terminate=True)
while True:
if len(ac1.direct_imap.idle_check(terminate=True)) > 1:
# If length is 1, it's [(b'OK', b'Still here')]
# Could happen on very slow network.
#
# More is usually [(1, b'EXISTS'), (1, b'RECENT')]
break
ac1.direct_imap.conn.move(["*"], folder) # "*" means "biggest UID in mailbox"
lp.sec("Everything prepared, now see if DeltaChat finds the message (" + variant + ")")

View File

@@ -1,6 +1,6 @@
//! # Account manager module.
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use async_std::channel::{self, Receiver, Sender};
use async_std::fs;
@@ -12,7 +12,7 @@ use uuid::Uuid;
use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use crate::context::Context;
use crate::context::{Context, ContextError};
use crate::events::{Event, EventType, Events};
/// Account manager, that can handle multiple accounts in a single place.
@@ -50,7 +50,7 @@ impl Accounts {
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
/// no account exists and no config exists.
pub async fn open(dir: PathBuf) -> Result<Self> {
async fn open(dir: PathBuf) -> Result<Self> {
ensure!(dir.exists().await, "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
@@ -91,6 +91,9 @@ impl Accounts {
}
/// Get the currently selected account.
///
/// If the selected account is encrypted and not yet loaded using
/// [`Accounts::load_encrypted_account`] `None` will be returned.
pub async fn get_selected_account(&self) -> Option<Context> {
let id = self.config.get_selected_account().await;
self.accounts.get(&id).cloned()
@@ -124,17 +127,41 @@ impl Accounts {
Ok(account_config.id)
}
/// Adds a new closed account.
pub async fn add_closed_account(&mut self) -> Result<u32> {
/// Adds an new encrypted account and opens it.
///
/// Creates a new account with encrypted database using the provided password. Returns
/// the account ID of the opened account.
pub async fn add_encrypted_account(&mut self, passphrase: String) -> Result<u32> {
let account_config = self.config.new_account(&self.dir).await?;
let ctx = Context::new_closed(account_config.dbfile().into(), account_config.id).await?;
let ctx = Context::new_encrypted(
account_config.dbfile().into(),
account_config.id,
passphrase,
)
.await?;
self.emitter.add_account(&ctx).await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
}
/// Decrypts and open an existing account.
pub async fn load_encrypted_account(&mut self, id: u32, passphrase: String) -> Result<Context> {
let account_config = self
.config
.get_account(id)
.await
.with_context(|| format!("No such account with id {}", id))?;
let ctx = Context::new_encrypted(
account_config.dbfile().into(),
account_config.id,
passphrase,
)
.await?;
self.emitter.add_account(&ctx).await?;
self.accounts.insert(id, ctx.clone());
Ok(ctx)
}
/// Remove an account.
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
let ctx = self.accounts.remove(&id);
@@ -228,6 +255,21 @@ impl Accounts {
self.accounts.keys().copied().collect()
}
/// Returns all encrypted accounts.
///
/// Note that we can't really distinguish between unreadable/corrupted accounts and
/// encrypted accounts. We consider all known accounts which failed to load encrypted,
/// they can be loaded using [`Accounts::load_encrypted_account`].
pub fn get_encrypted(&self) -> Vec<u32> {
let configured_ids: BTreeSet<u32> = self
.config
.all_configured_accounts()
.map(|cfg| cfg.id)
.collect();
let loaded_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
configured_ids.difference(&loaded_ids).copied().collect()
}
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
///
/// Returns whether all accounts finished their background work.
@@ -400,19 +442,29 @@ impl Config {
Ok(Config { file, inner })
}
/// Returns all account configurations.
fn all_configured_accounts(&self) -> impl Iterator<Item = &AccountConfig> {
self.inner.accounts.iter()
}
/// Loads all unencrypted accounts.
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
let mut accounts = BTreeMap::new();
for account_config in &self.inner.accounts {
let ctx = Context::new(account_config.dbfile().into(), account_config.id)
.await
.with_context(|| {
format!(
"failed to create context from file {:?}",
account_config.dbfile()
)
})?;
accounts.insert(account_config.id, ctx);
match Context::new(account_config.dbfile().into(), account_config.id).await {
Ok(ctx) => {
accounts.insert(account_config.id, ctx);
}
Err(ContextError::WrongKey) => {
continue;
}
Err(ContextError::Other(err)) => {
return Err(err.context(format!(
"failed to create context from file {}",
account_config.dbfile().display()
)));
}
}
}
Ok(accounts)
@@ -746,36 +798,73 @@ mod tests {
assert_eq!(accounts.accounts.len(), 0);
let account_id = accounts
.add_closed_account()
.add_encrypted_account("foobar".to_string())
.await
.context("failed to add closed account")?;
.context("failed to add encrypted account")?;
let account = accounts
.get_selected_account()
.await
.context("failed to get account")?;
assert_eq!(account.id, account_id);
let passphrase_set_success = account
.open("foobar".to_string())
.await
.context("failed to set passphrase")?;
assert!(passphrase_set_success);
drop(accounts);
let accounts = Accounts::new(p.clone())
let mut accounts = Accounts::new(p.clone())
.await
.context("failed to create second accounts manager")?;
assert!(accounts.get_selected_account().await.is_none());
let id = accounts
.get_selected_account_id()
.await
.context("failed to get selected account id")?;
// Try wrong passphrase
assert!(accounts
.load_encrypted_account(id, "barfoo".to_string())
.await
.is_err());
let loaded_account = accounts
.load_encrypted_account(id, "foobar".to_string())
.await
.context("failed to load encrypted account")?;
let account = accounts
.get_selected_account()
.await
.context("failed to get account")?;
assert_eq!(account.is_open().await, false);
assert_eq!(loaded_account.id, account.id);
// Try wrong passphrase.
assert_eq!(account.open("barfoo".to_string()).await?, false);
assert_eq!(account.open("".to_string()).await?, false);
Ok(())
}
assert_eq!(account.open("foobar".to_string()).await?, true);
assert_eq!(account.is_open().await, true);
#[async_std::test]
async fn test_get_encrypted() -> Result<()> {
let dir = tempfile::tempdir().context("failed to create tempdir")?;
let p: PathBuf = dir.path().join("accounts").into();
let mut accounts = Accounts::new(p.clone())
.await
.context("failed to create accounts manager")?;
let account_id = accounts
.add_encrypted_account("secret".to_string())
.await
.context("failed to add encrypted account")?;
drop(accounts);
let mut accounts = Accounts::new(p.clone())
.await
.context("failed to create second accounts manager")?;
let encrypted_ids = accounts.get_encrypted();
assert_eq!(vec![account_id], encrypted_ids);
for id in encrypted_ids {
let res = accounts
.load_encrypted_account(id, "secret".to_string())
.await;
assert!(res.is_ok());
}
let encrypted_ids = accounts.get_encrypted();
assert!(encrypted_ids.is_empty());
Ok(())
}

View File

@@ -474,22 +474,21 @@ impl ChatId {
self
);
if visibility == ChatVisibility::Archived {
context
.sql
.execute(
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
)
.await?;
}
context
.sql
.execute(
"UPDATE chats SET archived=? WHERE id=?;",
paramsv![visibility, self],
)
.transaction(move |transaction| {
if visibility == ChatVisibility::Archived {
transaction.execute(
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
)?;
}
transaction.execute(
"UPDATE chats SET archived=? WHERE id=?;",
paramsv![visibility, self],
)?;
Ok(())
})
.await?;
context.emit_event(EventType::MsgsChanged {
@@ -961,7 +960,7 @@ impl std::fmt::Display for ChatId {
/// well as query for a [ChatId].
impl rusqlite::types::ToSql for ChatId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(self.0 as i64);
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
@@ -971,7 +970,7 @@ impl rusqlite::types::ToSql for ChatId {
impl rusqlite::types::FromSql for ChatId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|val| {
if 0 <= val && val <= std::u32::MAX as i64 {
if 0 <= val && val <= i64::from(std::u32::MAX) {
Ok(ChatId::new(val as u32))
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(val))

View File

@@ -74,6 +74,13 @@ pub enum Config {
#[strum(props(default = "0"))]
SentboxMove, // If `MvboxMove` is true, this config is ignored. Currently only used in tests.
/// Watch for new messages in the "Mvbox" (aka DeltaChat folder) only.
///
/// This will not entirely disable other folders, e.g. the spam folder will also still
/// be watched for new messages.
#[strum(props(default = "0"))]
OnlyFetchMvbox,
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
ShowEmails,
@@ -225,6 +232,11 @@ impl Context {
Ok(self.get_config_int(key).await? != 0)
}
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
@@ -233,7 +245,7 @@ impl Context {
match self.get_config_int(Config::DeleteServerAfter).await? {
0 => Ok(None),
1 => Ok(Some(0)),
x => Ok(Some(x as i64)),
x => Ok(Some(i64::from(x))),
}
}
@@ -255,7 +267,7 @@ impl Context {
pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteDeviceAfter).await? {
0 => Ok(None),
x => Ok(Some(x as i64)),
x => Ok(Some(i64::from(x))),
}
}
@@ -281,31 +293,25 @@ impl Context {
}
}
self.emit_event(EventType::SelfavatarChanged);
Ok(())
}
Config::DeleteDeviceAfter => {
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
let ret = self.sql.set_raw_config(key, value).await;
// Force chatlist reload to delete old messages immediately.
self.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
ret
ret?
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql.set_raw_config(key, value.as_deref()).await?;
Ok(())
}
_ => {
self.sql.set_raw_config(key, value).await?;
Ok(())
}
}
Ok(())
}
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {

View File

@@ -443,7 +443,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 900);
let create_mvbox = ctx.get_config_bool(Config::MvboxMove).await?;
let create_mvbox = ctx.should_watch_mvbox().await?;
imap.configure_folders(ctx, create_mvbox).await?;

View File

@@ -5,7 +5,7 @@ use std::ffi::OsString;
use std::ops::Deref;
use std::time::{Instant, SystemTime};
use anyhow::{bail, ensure, Result};
use anyhow::{bail, Context as _, Result};
use async_std::{
channel::{self, Receiver, Sender},
path::{Path, PathBuf},
@@ -25,7 +25,7 @@ use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::scheduler::Scheduler;
use crate::securejoin::Bob;
use crate::sql::Sql;
use crate::sql::{Sql, SqlOpenError};
#[derive(Clone, Debug)]
pub struct Context {
@@ -103,74 +103,62 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
res
}
#[derive(Debug, thiserror::Error)]
pub enum ContextError {
#[error("wrong passphrase or unencrypted context")]
WrongKey,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl From<SqlOpenError> for ContextError {
fn from(source: SqlOpenError) -> Self {
match source {
SqlOpenError::WrongKey => Self::WrongKey,
SqlOpenError::Other(err) => Self::Other(err),
}
}
}
impl Context {
/// Creates new context and opens the database.
pub async fn new(dbfile: PathBuf, id: u32) -> Result<Context> {
let context = Self::new_closed(dbfile, id).await?;
// Open the database if is not encrypted.
if context.check_passphrase("".to_string()).await? {
context.sql.open(&context, "".to_string()).await?;
}
Ok(context)
pub async fn new(dbfile: PathBuf, id: u32) -> Result<Context, ContextError> {
Context::new_common(dbfile, id, None).await
}
/// Creates new context without opening the database.
pub async fn new_closed(dbfile: PathBuf, id: u32) -> Result<Context> {
pub async fn new_encrypted(
dbfile: PathBuf,
id: u32,
passphrase: String,
) -> Result<Context, ContextError> {
Context::new_common(dbfile, id, Some(passphrase)).await
}
async fn new_common(
dbfile: PathBuf,
id: u32,
passphrase: Option<String>,
) -> Result<Context, ContextError> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
let blobdir = dbfile.with_file_name(blob_fname);
if !blobdir.exists().await {
async_std::fs::create_dir_all(&blobdir).await?;
async_std::fs::create_dir_all(&blobdir)
.await
.context("Failed to create blobdir")?;
}
let context = Context::with_blobdir(dbfile, blobdir, id).await?;
Ok(context)
}
/// Opens the database with the given passphrase.
///
/// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other
/// errors.
pub async fn open(&self, passphrase: String) -> Result<bool> {
if self.sql.check_passphrase(passphrase.clone()).await? {
self.sql.open(self, passphrase).await?;
Ok(true)
} else {
Ok(false)
}
}
/// Returns true if database is open.
pub async fn is_open(&self) -> bool {
self.sql.is_open().await
}
/// Tests the database passphrase.
///
/// Returns true if passphrase is correct.
///
/// Fails if database is already open.
pub(crate) async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
self.sql.check_passphrase(passphrase).await
}
pub(crate) async fn with_blobdir(
dbfile: PathBuf,
blobdir: PathBuf,
id: u32,
) -> Result<Context> {
ensure!(
blobdir.is_dir().await,
"Blobdir does not exist: {}",
blobdir.display()
);
let sql = match passphrase {
Some(passphrase) => Sql::new_encrypted(dbfile, passphrase).await?,
None => Sql::new(dbfile).await?,
};
let inner = InnerContext {
id,
blobdir,
running_state: RwLock::new(Default::default()),
sql: Sql::new(dbfile),
sql,
bob: Default::default(),
last_smeared_timestamp: RwLock::new(0),
generating_key_mutex: Mutex::new(()),
@@ -189,6 +177,7 @@ impl Context {
let ctx = Context {
inner: Arc::new(inner),
};
ctx.sql.run_migrations(&ctx).await?;
Ok(ctx)
}
@@ -358,6 +347,7 @@ impl Context {
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
let only_fetch_mvbox = self.get_config_int(Config::OnlyFetchMvbox).await?;
let folders_configured = self
.sql
.get_raw_config_int("folders_configured")
@@ -422,6 +412,7 @@ impl Context {
res.insert("sentbox_watch", sentbox_watch.to_string());
res.insert("mvbox_move", mvbox_move.to_string());
res.insert("sentbox_move", sentbox_move.to_string());
res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string());
res.insert("folders_configured", folders_configured.to_string());
res.insert("configured_sentbox_folder", configured_sentbox_folder);
res.insert("configured_mvbox_folder", configured_mvbox_folder);
@@ -678,21 +669,16 @@ mod tests {
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
use crate::message::Message;
use crate::test_utils::TestContext;
use anyhow::Context as _;
use std::time::Duration;
use strum::IntoEnumIterator;
use tempfile::tempdir;
#[async_std::test]
async fn test_wrong_db() -> Result<()> {
let tmp = tempfile::tempdir()?;
async fn test_wrong_db() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123")?;
let res = Context::new(dbfile.into(), 1).await?;
// Broken database is indistinguishable from encrypted one.
assert_eq!(res.is_open().await, false);
Ok(())
std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new(dbfile.into(), 1).await;
assert!(res.is_err());
}
#[async_std::test]
@@ -847,16 +833,6 @@ mod tests {
assert!(blobdir.is_dir());
}
#[async_std::test]
async fn test_wrong_blogdir() {
let tmp = tempfile::tempdir().unwrap();
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(dbfile.into(), 1).await;
assert!(res.is_err());
}
#[async_std::test]
async fn test_sqlite_parent_not_exists() {
let tmp = tempfile::tempdir().unwrap();
@@ -868,24 +844,6 @@ mod tests {
assert!(dbfile2.is_file());
}
#[async_std::test]
async fn test_with_empty_blobdir() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(dbfile.into(), blobdir, 1).await;
assert!(res.is_err());
}
#[async_std::test]
async fn test_with_blobdir_not_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(dbfile.into(), blobdir.into(), 1).await;
assert!(res.is_err());
}
#[async_std::test]
async fn no_crashes_on_context_deref() {
let t = TestContext::new().await;
@@ -1046,27 +1004,42 @@ mod tests {
}
#[async_std::test]
async fn test_check_passphrase() -> Result<()> {
let dir = tempdir()?;
async fn test_reopen_unencrypted() {
let dir = tempfile::tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 1;
let context = Context::new_closed(dbfile.clone().into(), id)
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
assert_eq!(context.is_open().await, true);
drop(context);
// First time: creates new db.
Context::new(dbfile.clone().into(), 1).await.unwrap();
let id = 2;
let context = Context::new(dbfile.into(), id)
.await
.context("failed to create context")?;
assert_eq!(context.is_open().await, false);
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
assert_eq!(context.open("false".to_string()).await?, false);
assert_eq!(context.open("foo".to_string()).await?, true);
// Opening it encrypted should now fail.
let res = Context::new_encrypted(dbfile.clone().into(), 1, "secret".to_string()).await;
assert!(matches!(res, Err(ContextError::WrongKey)));
Ok(())
// But opening normally still works.
let res = Context::new(dbfile.into(), 1).await;
assert!(res.is_ok());
}
#[async_std::test]
async fn test_reopen_ecrypted() {
let dir = tempfile::tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
// First time: creates new db.
Context::new_encrypted(dbfile.clone().into(), 1, "secret".to_string())
.await
.unwrap();
// Opening it unencrypted should now fail.
let res = Context::new(dbfile.clone().into(), 1).await;
assert!(matches!(res, Err(ContextError::WrongKey)));
// Wrong password also fails.
let res = Context::new_encrypted(dbfile.clone().into(), 1, "oops".to_string()).await;
assert!(matches!(res, Err(ContextError::WrongKey)));
// Finally using the right password still works.
let res = Context::new_encrypted(dbfile.into(), 1, "secret".to_string()).await;
assert!(res.is_ok());
}
}

View File

@@ -1156,8 +1156,8 @@ INSERT INTO msgs
stmt.execute(paramsv![
rfc724_mid,
chat_id,
if trash { 0 } else { from_id as i32 },
if trash { 0 } else { to_id as i32 },
if trash { 0 } else { i64::from(from_id) },
if trash { 0 } else { i64::from(to_id) },
sort_timestamp,
sent_timestamp,
rcvd_timestamp,

View File

@@ -71,7 +71,7 @@ pub(crate) fn dc_gm2local_offset() -> i64 {
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
the function may return negative values. */
let lt = Local::now();
lt.offset().local_minus_utc() as i64
i64::from(lt.offset().local_minus_utc())
}
// timesmearing

View File

@@ -451,12 +451,11 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
.execute(
"UPDATE imap
SET target=''
WHERE EXISTS (
SELECT * FROM msgs
WHERE rfc724_mid=imap.rfc724_mid
AND ((download_state = 0 AND timestamp < ?) OR
(download_state != 0 AND timestamp < ?) OR
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
WHERE rfc724_mid IN (
SELECT rfc724_mid FROM msgs
WHERE ((download_state = 0 AND timestamp < ?) OR
(download_state != 0 AND timestamp < ?) OR
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
)",
paramsv![threshold_timestamp, threshold_timestamp_extended, now],
)

View File

@@ -9,7 +9,7 @@ use std::{
collections::{BTreeMap, BTreeSet},
};
use anyhow::{anyhow, bail, format_err, Context as _, Result};
use anyhow::{bail, format_err, Context as _, Result};
use async_imap::types::{
Fetch, Flag, Mailbox, Name, NameAttribute, Quota, QuotaRoot, UnsolicitedResponse,
};
@@ -464,9 +464,6 @@ impl Imap {
self.delete_messages(context, watch_folder)
.await
.context("delete_messages")?;
self.sync_seen_flags(context, watch_folder)
.await
.context("sync_seen_flags")?;
Ok(())
}
@@ -667,6 +664,11 @@ impl Imap {
folder: &str,
fetch_existing_msgs: bool,
) -> Result<bool> {
if should_ignore_folder(context, folder).await? {
info!(context, "Not fetching from {}", folder);
return Ok(false);
}
let new_emails = self.select_with_uidvalidity(context, folder).await?;
if !new_emails && !fetch_existing_msgs {
@@ -825,18 +827,98 @@ impl Imap {
Ok(read_cnt > 0)
}
/// Moves batch of messages identified by their UID from the currently
/// selected folder to the target folder.
async fn move_message_batch(
&mut self,
context: &Context,
set: &str,
row_ids: Vec<i64>,
target: &str,
) -> Result<()> {
if self.config.can_move {
let session = self
.session
.as_mut()
.context("no session while attempting to MOVE messages")?;
match session.uid_mv(set, &target).await {
Ok(()) => {
// Messages are moved or don't exist, IMAP returns OK response in both cases.
context
.sql
.execute(
format!(
"DELETE FROM imap WHERE id IN ({})",
row_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
),
rusqlite::params_from_iter(row_ids),
)
.await
.context("cannot delete moved messages from imap table")?;
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP messages {} moved to {}",
set, target
)));
return Ok(());
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {} to {}: {}",
set,
target,
err
);
}
}
} else {
info!(
context,
"Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
);
}
// Server does not support MOVE or MOVE failed.
// Copy the message to the destination folder and mark the record for deletion.
let session = self
.session
.as_mut()
.context("no session while attempting to COPY messages")?;
match session.uid_copy(&set, &target).await {
Ok(()) => {
context
.sql
.execute(
format!(
"UPDATE imap SET target='' WHERE id IN ({})",
row_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
),
rusqlite::params_from_iter(row_ids),
)
.await
.context("cannot plan deletion of copied messages")?;
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP messages {} copied to {}",
set, target
)));
Ok(())
}
Err(err) => Err(err.into()),
}
}
/// Moves messages.
///
/// This is the only place where messages are moved on the IMAP server.
async fn move_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
let rows = context
let mut rows = context
.sql
.query_map(
"SELECT id, uid, target FROM imap
WHERE folder = ?
AND target != folder
AND target != '' -- Not planned for deletion.
ORDER BY id",
ORDER BY target, uid",
paramsv![folder],
|row| {
let rowid: i64 = row.get(0)?;
@@ -846,105 +928,57 @@ impl Imap {
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
.await?
.into_iter()
.peekable();
self.prepare(context).await?;
self.select_folder(context, Some(folder)).await?;
for (rowid, uid, target) in rows {
// TODO: batch moves of messages with the same destination.
let set = uid.to_string();
while let Some((_, _, target)) = rows.peek().cloned() {
// Construct next request for the target folder.
let mut uid_set = String::new();
let mut rowid_set = Vec::new();
if self.config.can_move {
if let Some(session) = &mut self.session {
match session.uid_mv(&set, &target).await {
Ok(_) => {
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP message {}/{} moved to {}",
folder, uid, target
)));
context
.sql
.execute("DELETE FROM imap WHERE id=?", paramsv![rowid])
.await?;
continue;
}
Err(async_imap::error::Error::No(text)) => {
// "NO" response, probably the message is moved already.
info!(
context,
"IMAP message {}/{} cannot be moved: {}", folder, uid, text
);
context
.sql
.execute("DELETE FROM imap WHERE id=?", paramsv![rowid])
.await?;
continue;
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
folder,
uid,
target,
err
);
}
while uid_set.len() < 1000 {
// Construct a new range.
if let Some((start_rowid, start_uid, _)) =
rows.next_if(|(_, _, start_target)| start_target == &target)
{
rowid_set.push(start_rowid);
let mut end_uid = start_uid;
while let Some((next_rowid, next_uid, _)) =
rows.next_if(|(_, next_uid, next_target)| {
next_target == &target && *next_uid == end_uid + 1
})
{
end_uid = next_uid;
rowid_set.push(next_rowid);
}
let uid_range = UidRange {
start: start_uid,
end: end_uid,
};
if !uid_set.is_empty() {
uid_set.push(',');
}
uid_set.push_str(&uid_range.to_string());
} else {
bail!("No session while attempting to move the message");
break;
}
} else {
info!(
context,
"Server does not support MOVE, fallback to COPY/DELETE {}/{} to {}",
folder,
uid,
target
);
}
// Server does not support MOVE or MOVE failed.
// Copy the message to the destination folder and mark the record for deletion.
if let Some(session) = &mut self.session {
match session.uid_copy(&set, &target).await {
Ok(_) => {
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP message {}/{} copied to {}",
folder, uid, target
)));
// Plan deletion of the original message.
context
.sql
.execute("UPDATE imap SET target='' WHERE id=?", paramsv![rowid])
.await?;
}
Err(async_imap::error::Error::No(text)) => {
// "NO" response, probably the message is moved already.
info!(
context,
"IMAP message {}/{} cannot be copied: {}", folder, uid, text
);
context
.sql
.execute("DELETE FROM imap WHERE id=?", paramsv![rowid])
.await?;
continue;
}
Err(err) => {
warn!(
context,
"Could not copy message {}/{}: {}", folder, uid, err
);
// Break the loop to avoid moving messages out of order.
// We can't proceed until this message is moved or copied.
break;
}
}
} else {
bail!("No session while attempting to copy the message");
}
// Execute request.
self.move_message_batch(context, &uid_set, rowid_set, &target)
.await
.with_context(|| {
format!(
"cannot move batch of messages {:?} to folder {:?}",
&uid_set, target
)
})?;
}
Ok(())
@@ -1023,33 +1057,23 @@ impl Imap {
.as_ref()
.with_context(|| format!("No mailbox selected, folder: {}", folder))?;
let remote_highest_modseq = if let Some(remote_highest_modseq) = mailbox.highest_modseq {
remote_highest_modseq
} else {
// Check if the mailbox supports MODSEQ.
// We are not interested in actual value of HIGHESTMODSEQ.
if mailbox.highest_modseq.is_none() {
info!(
context,
"Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
);
return Ok(());
};
let mut highest_modseq = get_modseq(context, folder)
.await
.with_context(|| format!("failed to get MODSEQ for folder {}", folder))?;
if highest_modseq >= remote_highest_modseq {
info!(
context,
"MODSEQ {} is already new, HIGHESTMODSEQ={}, skipping seen flag update",
highest_modseq,
remote_highest_modseq
);
return Ok(());
}
let mut updated_chat_ids = BTreeSet::new();
let uid_validity = get_uidvalidity(context, folder)
.await
.with_context(|| format!("failed to get UID validity for folder {}", folder))?;
let mut highest_modseq = get_modseq(context, folder)
.await
.with_context(|| format!("failed to get MODSEQ for folder {}", folder))?;
let mut list = session
.uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {})", highest_modseq))
.await
@@ -1084,10 +1108,6 @@ impl Imap {
}
}
if remote_highest_modseq > highest_modseq {
// We haven't seen the message with the highest MODSEQ, maybe it was deleted already.
highest_modseq = remote_highest_modseq;
}
set_modseq(context, folder, highest_modseq)
.await
.with_context(|| format!("failed to set MODSEQ for folder {}", folder))?;
@@ -1295,7 +1315,8 @@ impl Imap {
let folder = folder.clone();
// safe, as we checked above that there is a body.
let body = body.unwrap();
let body = body
.context("we checked that message has body right above, but it has vanished")?;
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
match dc_receive_imf_inner(
@@ -1350,7 +1371,7 @@ impl Imap {
bail!("Can't set flag, should reconnect");
}
let session = self.session.as_mut().context("No session").unwrap();
let session = self.session.as_mut().context("No session")?;
let query = format!("+FLAGS ({})", flag);
let mut responses = session
.uid_store(uid_set, &query)
@@ -1584,23 +1605,6 @@ impl Imap {
Ok(())
}
/// Update HIGHESTMODSEQ on selected mailbox.
///
/// Should be called when MODSEQ is seen on the response, such as IDLE response.
pub(crate) fn update_modseq(&mut self, modseq: u64) {
self.config.selected_mailbox =
self.config
.selected_mailbox
.as_ref()
.map(|mailbox| Mailbox {
highest_modseq: Some(std::cmp::max(
mailbox.highest_modseq.unwrap_or_default(),
modseq,
)),
..mailbox.clone()
});
}
/// Return whether the server sent an unsolicited EXISTS response.
/// Drains all responses from `session.unsolicited_responses` in the process.
/// If this returns `true`, this means that new emails arrived and you should
@@ -1627,16 +1631,13 @@ impl Imap {
self.config.can_check_quota
}
pub async fn get_quota_roots(
pub(crate) async fn get_quota_roots(
&mut self,
mailbox_name: &str,
) -> Result<(Vec<QuotaRoot>, Vec<Quota>)> {
if let Some(session) = self.session.as_mut() {
let quota_roots = session.get_quota_root(mailbox_name).await?;
Ok(quota_roots)
} else {
Err(anyhow!("Not connected to IMAP, no session"))
}
let session = self.session.as_mut().context("no session")?;
let quota_roots = session.get_quota_root(mailbox_name).await?;
Ok(quota_roots)
}
}
@@ -1670,7 +1671,11 @@ async fn spam_target_folder(
}
}
if needs_move_to_mvbox(context, headers).await? {
if needs_move_to_mvbox(context, headers).await?
// We don't want to move the message to the inbox or sentbox where we wouldn't
// fetch it again:
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
{
Ok(Some(Config::ConfiguredMvboxFolder))
} else if needs_move_to_sentbox(context, folder, headers).await? {
Ok(Some(Config::ConfiguredSentboxFolder))
@@ -2005,7 +2010,7 @@ async fn mark_seen_by_uid(
.sql
.query_row_optional(
"SELECT id, chat_id FROM msgs
WHERE rfc724_mid IN (
WHERE id > 9 AND rfc724_mid IN (
SELECT rfc724_mid FROM imap
WHERE folder=?1
AND uidvalidity=?2
@@ -2152,6 +2157,21 @@ pub async fn get_config_last_seen_uid(context: &Context, folder: &str) -> Result
}
}
/// Whether to ignore fetching messages from a folder.
///
/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders
/// not explicitly watched should not be fetched.
async fn should_ignore_folder(context: &Context, folder: &str) -> Result<bool> {
if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
return Ok(false);
}
if context.is_sentbox(folder).await? {
// Still respect the SentboxWatch setting.
return Ok(!context.get_config_bool(Config::SentboxWatch).await?);
}
Ok(!(context.is_mvbox(folder).await? || context.is_spam_folder(folder).await?))
}
/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)

View File

@@ -3,7 +3,6 @@ use super::Imap;
use anyhow::{bail, Context as _, Result};
use async_imap::extensions::idle::IdleResponse;
use async_std::prelude::*;
use imap_proto::types::{AttributeValue, Response};
use std::time::{Duration, SystemTime};
use crate::{context::Context, scheduler::InterruptInfo};
@@ -72,13 +71,6 @@ impl Imap {
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "Idle has NewData {:?}", x);
if let Response::Fetch(_message, attrs) = x.parsed() {
for attr in attrs {
if let AttributeValue::ModSeq(modseq) = attr {
self.update_modseq(*modseq);
}
}
}
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(context, "Idle-wait timeout or interruption");
@@ -165,7 +157,6 @@ impl Imap {
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
match self.fetch_new_messages(context, &watch_folder, false).await {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);

View File

@@ -10,7 +10,8 @@ use async_std::prelude::*;
use super::{get_folder_meaning, get_folder_meaning_by_name};
impl Imap {
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<()> {
/// Returns true if folders were scanned, false if scanning was postponed.
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<bool> {
// First of all, debounce to once per minute:
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
@@ -20,7 +21,7 @@ impl Imap {
.await?;
if elapsed_secs < debounce_secs {
return Ok(());
return Ok(false);
}
}
info!(context, "Starting full folder scan");
@@ -98,24 +99,26 @@ impl Imap {
}
last_scan.replace(Instant::now());
Ok(())
Ok(true)
}
}
pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
let mut res = vec![Config::ConfiguredInboxFolder];
if context.get_config_bool(Config::SentboxWatch).await? {
res.push(Config::ConfiguredSentboxFolder);
}
if context.should_watch_mvbox().await? {
res.push(Config::ConfiguredMvboxFolder);
}
Ok(res)
}
pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
let mut res = Vec::new();
if let Some(inbox_folder) = context.get_config(Config::ConfiguredInboxFolder).await? {
res.push(inbox_folder);
}
let folder_watched_configured = &[
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),
(Config::MvboxMove, Config::ConfiguredMvboxFolder),
];
for (watched, configured) in folder_watched_configured {
if context.get_config_bool(*watched).await? {
if let Some(folder) = context.get_config(*configured).await? {
res.push(folder);
}
for folder_config in get_watched_folder_configs(context).await? {
if let Some(folder) = context.get_config(folder_config).await? {
res.push(folder);
}
}
Ok(res)

View File

@@ -197,7 +197,7 @@ impl Job {
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
paramsv![
self.desired_timestamp,
self.tries as i64,
i64::from(self.tries),
self.param.to_string(),
self.job_id as i32,
],
@@ -676,7 +676,7 @@ fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
if seconds < 1 {
seconds = 1;
}
seconds as i64
i64::from(seconds)
}
}
}

View File

@@ -7,7 +7,8 @@
clippy::all,
clippy::indexing_slicing,
clippy::wildcard_imports,
clippy::needless_borrow
clippy::needless_borrow,
clippy::cast_lossless
)]
#![allow(
clippy::match_bool,

View File

@@ -253,7 +253,8 @@ impl LoginParam {
sql.set_raw_config(key, Some(&self.imap.server)).await?;
let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(key, self.imap.port as i32).await?;
sql.set_raw_config_int(key, i32::from(self.imap.port))
.await?;
let key = format!("{}mail_user", prefix);
sql.set_raw_config(key, Some(&self.imap.user)).await?;
@@ -273,7 +274,8 @@ impl LoginParam {
sql.set_raw_config(key, Some(&self.smtp.server)).await?;
let key = format!("{}send_port", prefix);
sql.set_raw_config_int(key, self.smtp.port as i32).await?;
sql.set_raw_config_int(key, i32::from(self.smtp.port))
.await?;
let key = format!("{}send_user", prefix);
sql.set_raw_config(key, Some(&self.smtp.user)).await?;

View File

@@ -183,7 +183,7 @@ impl rusqlite::types::ToSql for MsgId {
format_err!("Invalid MsgId {}", self.0).into(),
));
}
let val = rusqlite::types::Value::Integer(self.0 as i64);
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
@@ -194,7 +194,7 @@ impl rusqlite::types::FromSql for MsgId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
// Would be nice if we could use match here, but alas.
i64::column_result(value).and_then(|val| {
if 0 <= val && val <= std::u32::MAX as i64 {
if 0 <= val && val <= i64::from(std::u32::MAX) {
Ok(MsgId::new(val as u32))
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(val))

View File

@@ -366,7 +366,7 @@ static P_EXAMPLE_COM: Lazy<Provider> = Lazy::new(|| {
}
});
// fastmail.md: fastmail.com
// fastmail.md: 123mail.org, 150mail.com, 150ml.com, 16mail.com, 2-mail.com, 4email.net, 50mail.com, airpost.net, allmail.net, bestmail.us, cluemail.com, elitemail.org, emailcorner.net, emailengine.net, emailengine.org, emailgroups.net, emailplus.org, emailuser.net, eml.cc, f-m.fm, fast-email.com, fast-mail.org, fastem.com, fastemail.us, fastemailer.com, fastest.cc, fastimap.com, fastmail.cn, fastmail.co.uk, fastmail.com, fastmail.com.au, fastmail.de, fastmail.es, fastmail.fm, fastmail.fr, fastmail.im, fastmail.in, fastmail.jp, fastmail.mx, fastmail.net, fastmail.nl, fastmail.org, fastmail.se, fastmail.to, fastmail.tw, fastmail.uk, fastmail.us, fastmailbox.net, fastmessaging.com, fea.st, fmail.co.uk, fmailbox.com, fmgirl.com, fmguy.com, ftml.net, h-mail.us, hailmail.net, imap-mail.com, imap.cc, imapmail.org, inoutbox.com, internet-e-mail.com, internet-mail.org, internetemails.net, internetmailing.net, jetemail.net, justemail.net, letterboxes.org, mail-central.com, mail-page.com, mailandftp.com, mailas.com, mailbolt.com, mailc.net, mailcan.com, mailforce.net, mailftp.com, mailhaven.com, mailingaddress.org, mailite.com, mailmight.com, mailnew.com, mailsent.net, mailservice.ms, mailup.net, mailworks.org, ml1.net, mm.st, myfastmail.com, mymacmail.com, nospammail.net, ownmail.net, petml.com, postinbox.com, postpro.net, proinbox.com, promessage.com, realemail.net, reallyfast.biz, reallyfast.info, rushpost.com, sent.as, sent.at, sent.com, speedpost.net, speedymail.org, ssl-mail.com, swift-mail.com, the-fastest.net, the-quickest.com, theinternetemail.com, veryfast.biz, veryspeedy.net, warpmail.net, xsmail.com, yepmail.net, your-mail.com
static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
id: "fastmail",
status: Status::Preparation,
@@ -389,13 +389,6 @@ static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
port: 465,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "smtp.fastmail.com",
port: 587,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
@@ -716,8 +709,8 @@ static P_MAIL_DE: Lazy<Provider> = Lazy::new(|| Provider {
static P_MAIL_RU: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "mail.ru",
status: Status::Ok,
before_login_hint: "Не рекомендуется использовать mail.ru, потому что он разряжает вашу батарею быстрее, чем другие провайдеры.",
status: Status::Preparation,
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru, чтобы mail.ru работал с Delta Chat.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail-ru",
server: vec![
@@ -905,7 +898,7 @@ static P_NAVER: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de
static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| Provider {
id: "outlook.com",
status: Status::Ok,
@@ -1495,7 +1488,123 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("example.com", &*P_EXAMPLE_COM),
("example.org", &*P_EXAMPLE_COM),
("example.net", &*P_EXAMPLE_COM),
("123mail.org", &*P_FASTMAIL),
("150mail.com", &*P_FASTMAIL),
("150ml.com", &*P_FASTMAIL),
("16mail.com", &*P_FASTMAIL),
("2-mail.com", &*P_FASTMAIL),
("4email.net", &*P_FASTMAIL),
("50mail.com", &*P_FASTMAIL),
("airpost.net", &*P_FASTMAIL),
("allmail.net", &*P_FASTMAIL),
("bestmail.us", &*P_FASTMAIL),
("cluemail.com", &*P_FASTMAIL),
("elitemail.org", &*P_FASTMAIL),
("emailcorner.net", &*P_FASTMAIL),
("emailengine.net", &*P_FASTMAIL),
("emailengine.org", &*P_FASTMAIL),
("emailgroups.net", &*P_FASTMAIL),
("emailplus.org", &*P_FASTMAIL),
("emailuser.net", &*P_FASTMAIL),
("eml.cc", &*P_FASTMAIL),
("f-m.fm", &*P_FASTMAIL),
("fast-email.com", &*P_FASTMAIL),
("fast-mail.org", &*P_FASTMAIL),
("fastem.com", &*P_FASTMAIL),
("fastemail.us", &*P_FASTMAIL),
("fastemailer.com", &*P_FASTMAIL),
("fastest.cc", &*P_FASTMAIL),
("fastimap.com", &*P_FASTMAIL),
("fastmail.cn", &*P_FASTMAIL),
("fastmail.co.uk", &*P_FASTMAIL),
("fastmail.com", &*P_FASTMAIL),
("fastmail.com.au", &*P_FASTMAIL),
("fastmail.de", &*P_FASTMAIL),
("fastmail.es", &*P_FASTMAIL),
("fastmail.fm", &*P_FASTMAIL),
("fastmail.fr", &*P_FASTMAIL),
("fastmail.im", &*P_FASTMAIL),
("fastmail.in", &*P_FASTMAIL),
("fastmail.jp", &*P_FASTMAIL),
("fastmail.mx", &*P_FASTMAIL),
("fastmail.net", &*P_FASTMAIL),
("fastmail.nl", &*P_FASTMAIL),
("fastmail.org", &*P_FASTMAIL),
("fastmail.se", &*P_FASTMAIL),
("fastmail.to", &*P_FASTMAIL),
("fastmail.tw", &*P_FASTMAIL),
("fastmail.uk", &*P_FASTMAIL),
("fastmail.us", &*P_FASTMAIL),
("fastmailbox.net", &*P_FASTMAIL),
("fastmessaging.com", &*P_FASTMAIL),
("fea.st", &*P_FASTMAIL),
("fmail.co.uk", &*P_FASTMAIL),
("fmailbox.com", &*P_FASTMAIL),
("fmgirl.com", &*P_FASTMAIL),
("fmguy.com", &*P_FASTMAIL),
("ftml.net", &*P_FASTMAIL),
("h-mail.us", &*P_FASTMAIL),
("hailmail.net", &*P_FASTMAIL),
("imap-mail.com", &*P_FASTMAIL),
("imap.cc", &*P_FASTMAIL),
("imapmail.org", &*P_FASTMAIL),
("inoutbox.com", &*P_FASTMAIL),
("internet-e-mail.com", &*P_FASTMAIL),
("internet-mail.org", &*P_FASTMAIL),
("internetemails.net", &*P_FASTMAIL),
("internetmailing.net", &*P_FASTMAIL),
("jetemail.net", &*P_FASTMAIL),
("justemail.net", &*P_FASTMAIL),
("letterboxes.org", &*P_FASTMAIL),
("mail-central.com", &*P_FASTMAIL),
("mail-page.com", &*P_FASTMAIL),
("mailandftp.com", &*P_FASTMAIL),
("mailas.com", &*P_FASTMAIL),
("mailbolt.com", &*P_FASTMAIL),
("mailc.net", &*P_FASTMAIL),
("mailcan.com", &*P_FASTMAIL),
("mailforce.net", &*P_FASTMAIL),
("mailftp.com", &*P_FASTMAIL),
("mailhaven.com", &*P_FASTMAIL),
("mailingaddress.org", &*P_FASTMAIL),
("mailite.com", &*P_FASTMAIL),
("mailmight.com", &*P_FASTMAIL),
("mailnew.com", &*P_FASTMAIL),
("mailsent.net", &*P_FASTMAIL),
("mailservice.ms", &*P_FASTMAIL),
("mailup.net", &*P_FASTMAIL),
("mailworks.org", &*P_FASTMAIL),
("ml1.net", &*P_FASTMAIL),
("mm.st", &*P_FASTMAIL),
("myfastmail.com", &*P_FASTMAIL),
("mymacmail.com", &*P_FASTMAIL),
("nospammail.net", &*P_FASTMAIL),
("ownmail.net", &*P_FASTMAIL),
("petml.com", &*P_FASTMAIL),
("postinbox.com", &*P_FASTMAIL),
("postpro.net", &*P_FASTMAIL),
("proinbox.com", &*P_FASTMAIL),
("promessage.com", &*P_FASTMAIL),
("realemail.net", &*P_FASTMAIL),
("reallyfast.biz", &*P_FASTMAIL),
("reallyfast.info", &*P_FASTMAIL),
("rushpost.com", &*P_FASTMAIL),
("sent.as", &*P_FASTMAIL),
("sent.at", &*P_FASTMAIL),
("sent.com", &*P_FASTMAIL),
("speedpost.net", &*P_FASTMAIL),
("speedymail.org", &*P_FASTMAIL),
("ssl-mail.com", &*P_FASTMAIL),
("swift-mail.com", &*P_FASTMAIL),
("the-fastest.net", &*P_FASTMAIL),
("the-quickest.com", &*P_FASTMAIL),
("theinternetemail.com", &*P_FASTMAIL),
("veryfast.biz", &*P_FASTMAIL),
("veryspeedy.net", &*P_FASTMAIL),
("warpmail.net", &*P_FASTMAIL),
("xsmail.com", &*P_FASTMAIL),
("yepmail.net", &*P_FASTMAIL),
("your-mail.com", &*P_FASTMAIL),
("firemail.at", &*P_FIREMAIL_DE),
("firemail.de", &*P_FIREMAIL_DE),
("five.chat", &*P_FIVE_CHAT),
@@ -1539,6 +1648,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("office365.com", &*P_OUTLOOK_COM),
("outlook.com.tr", &*P_OUTLOOK_COM),
("live.com", &*P_OUTLOOK_COM),
("outlook.de", &*P_OUTLOOK_COM),
("posteo.de", &*P_POSTEO),
("posteo.af", &*P_POSTEO),
("posteo.at", &*P_POSTEO),
@@ -1743,4 +1853,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 1, 11));
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 1, 31));

View File

@@ -11,6 +11,7 @@ use crate::dc_tools::maybe_add_time_based_warnings;
use crate::ephemeral::delete_expired_imap_messages;
use crate::imap::Imap;
use crate::job::{self, Thread};
use crate::log::LogExt;
use crate::smtp::{send_smtp_messages, Smtp};
use self::connectivity::ConnectivityStore;
@@ -169,25 +170,49 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
warn!(ctx, "{:#}", err);
}
// Scan other folders before fetching from watched folder. This may result in the
// messages being moved into the watched folder, for example from the Spam folder to
// the Inbox folder.
if folder == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
if let Err(err) = connection.scan_folders(ctx).await {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{}", err);
}
}
// fetch
// Fetch the watched folder.
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
// Scan additional folders only after finishing fetching the watched folder.
//
// On iOS the application has strictly limited time to work in background, so we may not
// be able to scan all folders before time is up if there are many of them.
if folder == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
match connection.scan_folders(ctx).await {
Err(err) => {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{}", err);
}
Ok(true) => {
// Fetch the watched folder again in case scanning other folder moved messages
// there.
//
// In most cases this will select the watched folder and return because there are
// no new messages. We want to select the watched folder anyway before going IDLE
// there, so this does not take additional protocol round-trip.
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
}
Ok(false) => {}
}
}
// Synchronize Seen flags.
connection
.sync_seen_flags(ctx, &watch_folder)
.await
.context("sync_seen_flags")
.ok_or_log(ctx);
connection.connectivity.set_connected(ctx).await;
// idle
@@ -350,7 +375,7 @@ impl Scheduler {
}))
};
if ctx.get_config_bool(Config::MvboxMove).await? {
if ctx.should_watch_mvbox().await? {
let ctx = ctx.clone();
mvbox_handle = Some(task::spawn(async move {
simple_imap_loop(

View File

@@ -5,6 +5,7 @@ use async_std::sync::{Mutex, RwLockReadGuard};
use crate::dc_tools::time;
use crate::events::EventType;
use crate::imap::scan_folders::get_watched_folder_configs;
use crate::quota::{
QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_MAX_AGE_SECONDS, QUOTA_WARN_THRESHOLD_PERCENTAGE,
};
@@ -362,17 +363,14 @@ impl Context {
[
(
Config::ConfiguredInboxFolder,
None,
inbox.state.connectivity.clone(),
),
(
Config::ConfiguredMvboxFolder,
Some(Config::MvboxMove),
mvbox.state.connectivity.clone(),
),
(
Config::ConfiguredSentboxFolder,
Some(Config::SentboxWatch),
sentbox.state.connectivity.clone(),
),
],
@@ -391,20 +389,12 @@ impl Context {
// - "Sent": Connected
// =============================================================================================
let watched_folders = get_watched_folder_configs(self).await?;
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
for (folder, watch, state) in &folders_states {
let w = if let Some(watch_config) = *watch {
self.get_config(watch_config)
.await
.ok_or_log(self)
.flatten()
== Some("1".to_string())
} else {
true
};
for (folder, state) in &folders_states {
let mut folder_added = false;
if w {
if watched_folders.contains(folder) {
let f = self.get_config(*folder).await.ok_or_log(self).flatten();
if let Some(foldername) = f {

View File

@@ -7,10 +7,10 @@ use std::collections::HashSet;
use std::convert::TryFrom;
use std::time::Duration;
use anyhow::{bail, Context as _, Result};
use anyhow::{anyhow, Context as _, Result};
use async_std::path::PathBuf;
use async_std::prelude::*;
use rusqlite::{config::DbConfig, Connection, OpenFlags};
use rusqlite::{config::DbConfig, OpenFlags};
use crate::blob::BlobObject;
use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon};
@@ -49,40 +49,47 @@ pub struct Sql {
is_encrypted: RwLock<Option<bool>>,
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum SqlOpenError {
#[error("wrong passphrase or unencrypted context")]
WrongKey,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl Sql {
pub fn new(dbfile: PathBuf) -> Sql {
Self {
/// Opens or creates a new (unencrypted) database.
///
/// Note after creating this you **MUST** call [`Sql::run_migrations`].
pub(crate) async fn new(dbfile: PathBuf) -> Result<Sql, SqlOpenError> {
let sql = Self {
dbfile,
pool: Default::default(),
is_encrypted: Default::default(),
}
};
sql.open("".into()).await.map(|_| sql)
}
/// Tests SQLCipher passphrase.
/// Opens or creates an encrypted database.
///
/// Returns true if passphrase is correct, i.e. the database is new or can be unlocked with
/// this passphrase, and false if the database is already encrypted with another passphrase or
/// corrupted.
/// If the database does not exist this creates a new encrypted database with the
/// provided passphrase. If the database does exist attempts to open it with the
/// provided passphrase.
///
/// Fails if database is already open.
pub async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
if self.is_open().await {
bail!("Database is already opened.");
/// Note after creating this you **MUST** call [`Sql::run_migrations`].
pub(crate) async fn new_encrypted(
dbfile: PathBuf,
passphrase: String,
) -> Result<Sql, SqlOpenError> {
if passphrase.is_empty() {
return Err(anyhow!("Empty passphrase").into());
}
// Hold the lock to prevent other thread from opening the database.
let _lock = self.pool.write().await;
// Test that the key is correct using a single connection.
let connection = Connection::open(&self.dbfile)?;
connection
.pragma_update(None, "key", &passphrase)
.context("failed to set PRAGMA key")?;
let key_is_correct = connection
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.is_ok();
Ok(key_is_correct)
let sql = Self {
dbfile,
pool: Default::default(),
is_encrypted: Default::default(),
};
sql.open(passphrase).await.map(|_| sql)
}
/// Checks if there is currently a connection to the underlying Sqlite database.
@@ -194,11 +201,18 @@ impl Sql {
Ok(pool)
}
async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> {
*self.pool.write().await = Some(Self::new_pool(dbfile, passphrase.to_string())?);
async fn try_open(&self, dbfile: &Path, passphrase: String) -> Result<(), SqlOpenError> {
*self.pool.write().await = Some(Self::new_pool(dbfile, passphrase)?);
{
let conn = self.get_conn().await?;
let res = conn.query_row("SELECT count(*) FROM sqlite_master", params![], |_row| {
Ok(())
});
if res.is_err() {
// This hides SqliteFailure "NotADatabase"
return Err(SqlOpenError::WrongKey);
}
// Try to enable auto_vacuum. This will only be
// applied if the database is new or after successful
@@ -206,21 +220,22 @@ impl Sql {
// When auto_vacuum is INCREMENTAL, it is possible to
// use PRAGMA incremental_vacuum to return unused
// database pages to the filesystem.
conn.pragma_update(None, "auto_vacuum", &"INCREMENTAL".to_string())?;
conn.pragma_update(None, "auto_vacuum", &"INCREMENTAL".to_string())
.context("auto_vacuum pragma failed")?;
// journal_mode is persisted, it is sufficient to change it only for one handle.
conn.pragma_update(None, "journal_mode", &"WAL".to_string())?;
conn.pragma_update(None, "journal_mode", &"WAL".to_string())
.context("journal_mode pragma failed")?;
// Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode.
conn.pragma_update(None, "synchronous", &"NORMAL".to_string())?;
conn.pragma_update(None, "synchronous", &"NORMAL".to_string())
.context("synchronous pragma failed")?;
}
self.run_migrations(context).await?;
Ok(())
}
pub async fn run_migrations(&self, context: &Context) -> Result<()> {
pub(crate) async fn run_migrations(&self, context: &Context) -> Result<()> {
// (1) update low-level database structure.
// this should be done before updates that use high-level objects that
// rely themselves on the low-level structure.
@@ -290,28 +305,31 @@ impl Sql {
}
}
info!(context, "Migrations finished");
Ok(())
}
/// Opens the provided database and runs any necessary migrations.
/// Opens the provided database.
///
/// To open an unencrypted database provide an empty passphrase, if the passphrase is
/// wrong an error is returned.
///
/// If a database is already open, this will return an error.
pub async fn open(&self, context: &Context, passphrase: String) -> Result<()> {
async fn open(&self, passphrase: String) -> Result<(), SqlOpenError> {
if self.is_open().await {
error!(
context,
"Cannot open, database \"{:?}\" already opened.", self.dbfile,
);
bail!("SQL database is already opened.");
return Err(anyhow!("SQL database is already opened.").into());
}
let passphrase_nonempty = !passphrase.is_empty();
if let Err(err) = self.try_open(context, &self.dbfile, passphrase).await {
self.close().await;
Err(err)
} else {
info!(context, "Opened database {:?}.", self.dbfile);
*self.is_encrypted.write().await = Some(passphrase_nonempty);
Ok(())
match self.try_open(&self.dbfile, passphrase).await {
Ok(()) => {
*self.is_encrypted.write().await = Some(passphrase_nonempty);
Ok(())
}
Err(err) => {
self.close().await;
Err(err)
}
}
}
@@ -888,7 +906,7 @@ mod tests {
t.sql.close().await;
housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed
t.sql.open(&t, "".to_string()).await.unwrap();
t.sql.open("".to_string()).await.unwrap();
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
@@ -906,54 +924,10 @@ mod tests {
}
}
/// Regression test.
///
/// Previously the code checking for existence of `config` table
/// checked it with `PRAGMA table_info("config")` but did not
/// drain `SqlitePool.fetch` result, only using the first row
/// returned. As a result, prepared statement for `PRAGMA` was not
/// finalized early enough, leaving reader connection in a broken
/// state after reopening the database, when `config` table
/// existed and `PRAGMA` returned non-empty result.
///
/// Statements were not finalized due to a bug in sqlx:
/// <https://github.com/launchbadge/sqlx/issues/1147>
#[async_std::test]
async fn test_db_reopen() -> Result<()> {
use tempfile::tempdir;
// The context is used only for logging.
let t = TestContext::new().await;
// Create a separate empty database for testing.
let dir = tempdir()?;
let dbfile = dir.path().join("testdb.sqlite");
let sql = Sql::new(dbfile.into());
// Create database with all the tables.
sql.open(&t, "".to_string()).await.unwrap();
sql.close().await;
// Reopen the database
sql.open(&t, "".to_string()).await?;
sql.execute(
"INSERT INTO config (keyname, value) VALUES (?, ?);",
paramsv!("foo", "bar"),
)
.await?;
let value: Option<String> = sql
.query_get_value("SELECT value FROM config WHERE keyname=?;", paramsv!("foo"))
.await?;
assert_eq!(value.unwrap(), "bar");
Ok(())
}
#[async_std::test]
async fn test_migration_flags() -> Result<()> {
let t = TestContext::new().await;
t.evtracker.get_info_contains("Opened database").await;
t.evtracker.get_info_contains("Migrations finished").await;
// as migrations::run() was already executed on context creation,
// another call should not result in any action needed.
@@ -988,36 +962,4 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_check_passphrase() -> Result<()> {
use tempfile::tempdir;
// The context is used only for logging.
let t = TestContext::new().await;
// Create a separate empty database for testing.
let dir = tempdir()?;
let dbfile = dir.path().join("testdb.sqlite");
let sql = Sql::new(dbfile.clone().into());
sql.check_passphrase("foo".to_string()).await?;
sql.open(&t, "foo".to_string())
.await
.context("failed to open the database first time")?;
sql.close().await;
// Reopen the database
let sql = Sql::new(dbfile.into());
// Test that we can't open encrypted database without a passphrase.
assert!(sql.open(&t, "".to_string()).await.is_err());
// Now open the database with passpharse, it should succeed.
sql.check_passphrase("foo".to_string()).await?;
sql.open(&t, "foo".to_string())
.await
.context("failed to open the database second time")?;
Ok(())
}
}

View File

@@ -75,7 +75,7 @@ impl StatusUpdateId {
impl rusqlite::types::ToSql for StatusUpdateId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(self.0 as i64);
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}