Merge pull request #1356 from deltachat/feat/async-jobs

asyncify core & remove manual thread handling
This commit is contained in:
bjoern
2020-05-27 18:17:51 +02:00
committed by GitHub
86 changed files with 12337 additions and 11484 deletions

View File

@@ -10,7 +10,7 @@ jobs:
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: minimal
toolchain: nightly-2020-03-12 toolchain: nightly-2020-03-19
override: true override: true
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
@@ -25,7 +25,7 @@ jobs:
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: minimal
toolchain: nightly-2020-03-12 toolchain: nightly-2020-03-19
override: true override: true
- run: rustup component add rustfmt - run: rustup component add rustfmt
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
@@ -39,7 +39,7 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: nightly-2020-03-12 toolchain: nightly-2020-03-19
components: clippy components: clippy
override: true override: true
- uses: actions-rs/clippy-check@v1 - uses: actions-rs/clippy-check@v1

54
ASYNC-API-TODO.txt Normal file
View File

@@ -0,0 +1,54 @@
Delta Chat ASYNC (friedel, bjoern, floris, friedel)
- smtp fake-idle/load jobs gerade noch alle fuenf sekunden , sollte alle zehn minuten (oder gar nicht)
APIs:
dc_context_new # opens the database
dc_open # FFI only
-> drop it and move parameters to dc_context_new()
dc_configure # note: dc_start_jobs() is NOT allowed to run concurrently
dc_imex NEVER goes through the job system
dc_imex import_backup needs to ensure dc_stop_jobs()
dc_start_io # start smtp/imap and job handling subsystems
dc_stop_io # stop smtp/imap and job handling subsystems
dc_is_io_running # return 1 if smtp/imap/jobs susbystem is running
dc_close # FFI only
-> can be dropped
dc_context_unref
for ios share-extension:
Int dc_direct_send() -> try send out without going through jobs system, but queue a job in db if it needs to be retried on failure
0: message was sent
1: message failed to go out, is queued as a job to be retried later
2: message permanently failed?
EVENT handling:
start a callback thread and call get_next_event() which is BLOCKING
it's fine to start this callback thread later, it will see all events.
Note that the core infinitely fills the internal queue if you never drain it.
FFI-get_next_event() returns NULL if the context is unrefed already?
sidenote: how python's callback thread does it currently:
CB-thread runs this while loop:
while not QUITFLAG:
ev = context.get_next_event( )
...
So in order to shutdown properly one has to set QUITFLAG
before calling dc_stop_jobs() and dc_context_unref
event API:
get_data1_int
get_data2_int
get_data3_str
- userdata likely only used for the callbacks, likely can be dropped, needs verification
- iOS needs for the share app to call "try_send_smtp" wihtout a full dc_context_run and without going

2892
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ edition = "2018"
license = "MPL-2.0" license = "MPL-2.0"
[profile.release] [profile.release]
lto = true # lto = true
[dependencies] [dependencies]
deltachat_derive = { path = "./deltachat_derive" } deltachat_derive = { path = "./deltachat_derive" }
@@ -17,15 +17,15 @@ hex = "0.4.0"
sha2 = "0.8.0" sha2 = "0.8.0"
rand = "0.7.0" rand = "0.7.0"
smallvec = "1.0.0" smallvec = "1.0.0"
reqwest = { version = "0.10.0", features = ["blocking", "json"] } surf = { version = "2.0.0-alpha.2", default-features = false, features = ["h1-client"] }
num-derive = "0.3.0" num-derive = "0.3.0"
num-traits = "0.2.6" num-traits = "0.2.6"
async-smtp = "0.2" async-smtp = { version = "0.3" }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" } email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" } lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
async-imap = "0.2" async-imap = "0.3.1"
async-native-tls = "0.3.1" async-native-tls = { version = "0.3.3" }
async-std = { version = "1.4", features = ["unstable"] } async-std = { version = "1.6.0", features = ["unstable"] }
base64 = "0.11" base64 = "0.11"
charset = "0.1" charset = "0.1"
percent-encoding = "2.0" percent-encoding = "2.0"
@@ -35,12 +35,11 @@ chrono = "0.4.6"
indexmap = "1.3.0" indexmap = "1.3.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
regex = "1.1.6" regex = "1.1.6"
rusqlite = { version = "0.21", features = ["bundled"] } rusqlite = { version = "0.22", features = ["bundled"] }
r2d2_sqlite = "0.13.0" r2d2_sqlite = "0.15.0"
r2d2 = "0.8.5" r2d2 = "0.8.5"
strum = "0.16.0" strum = "0.16.0"
strum_macros = "0.16.0" strum_macros = "0.16.0"
thread-local-object = "0.1.0"
backtrace = "0.3.33" backtrace = "0.3.33"
byteorder = "1.3.1" byteorder = "1.3.1"
itertools = "0.8.0" itertools = "0.8.0"
@@ -55,17 +54,25 @@ mailparse = "0.12.0"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" } encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3" native-tls = "0.2.3"
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] } image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
pretty_env_logger = "0.3.1" futures = "0.3.4"
rustyline = { version = "4.1.0", optional = true }
thiserror = "1.0.14" thiserror = "1.0.14"
anyhow = "1.0.28" anyhow = "1.0.28"
async-trait = "0.1.31"
url = "2.1.1"
pretty_env_logger = { version = "0.3.1", optional = true }
log = {version = "0.4.8", optional = true }
rustyline = { version = "4.1.0", optional = true }
ansi_term = { version = "0.12.1", optional = true }
[dev-dependencies] [dev-dependencies]
tempfile = "3.0" tempfile = "3.0"
pretty_assertions = "0.6.1" pretty_assertions = "0.6.1"
pretty_env_logger = "0.3.0" pretty_env_logger = "0.3.0"
proptest = "0.9.4" proptest = "0.9.4"
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
smol = "0.1.10"
[workspace] [workspace]
members = [ members = [
@@ -76,15 +83,20 @@ members = [
[[example]] [[example]]
name = "simple" name = "simple"
path = "examples/simple.rs" path = "examples/simple.rs"
required-features = ["repl"]
[[example]] [[example]]
name = "repl" name = "repl"
path = "examples/repl/main.rs" path = "examples/repl/main.rs"
required-features = ["rustyline"] required-features = ["repl"]
[features] [features]
default = ["nightly"] default = []
vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"] internals = []
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term"]
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
nightly = ["pgp/nightly"] nightly = ["pgp/nightly"]
[patch.crates-io]
smol = { git = "https://github.com/dignifiedquire/smol-1", branch = "isolate-nix" }

View File

@@ -9,7 +9,7 @@
To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment: To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment:
``` ```
curl https://sh.rustup.rs -sSf | sh $ curl https://sh.rustup.rs -sSf | sh
``` ```
## Using the CLI client ## Using the CLI client
@@ -17,7 +17,7 @@ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`: Compile and run Delta Chat Core command line utility, using `cargo`:
``` ```
cargo run --example repl -- ~/deltachat-db $ RUST_LOG=info cargo run --example repl --features repl -- ~/deltachat-db
``` ```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist. where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.

View File

@@ -4,7 +4,7 @@ environment:
install: install:
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
- rustup-init -yv --default-toolchain nightly-2020-03-12 - rustup-init -yv --default-toolchain nightly-2020-03-19
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin - set PATH=%PATH%;%USERPROFILE%\.cargo\bin
- rustc -vV - rustc -vV
- cargo -vV - cargo -vV

View File

@@ -1,4 +1,4 @@
FROM quay.io/pypa/manylinux1_x86_64 FROM quay.io/pypa/manylinux2010_x86_64
# Configure ld.so/ldconfig and pkg-config # Configure ld.so/ldconfig and pkg-config
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \ RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \

View File

@@ -3,9 +3,9 @@
set -e -x set -e -x
# Install Rust # Install Rust
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2020-03-12 -y curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.43.1-x86_64-unknown-linux-gnu -y
export PATH=/root/.cargo/bin:$PATH export PATH=/root/.cargo/bin:$PATH
rustc --version rustc --version
# remove some 300-400 MB that we don't need for automated builds # remove some 300-400 MB that we don't need for automated builds
rm -rf /root/.rustup/toolchains/nightly-2020-03-12-x86_64-unknown-linux-gnu/share/ rm -rf /root/.rustup/toolchains/1.43.1-x86_64-unknown-linux-gnu/share

View File

@@ -46,6 +46,6 @@ echo "--- Running $CIRCLE_JOB remotely"
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run" ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
mkdir -p workspace mkdir -p workspace
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux1*" workspace/wheelhouse/ rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/ rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs

View File

@@ -21,7 +21,8 @@ export DCC_RS_DEV=$(pwd)
export PATH=$PATH:/opt/python/cp35-cp35m/bin export PATH=$PATH:/opt/python/cp35-cp35m/bin
export PYTHONDONTWRITEBYTECODE=1 export PYTHONDONTWRITEBYTECODE=1
pushd /bin pushd /bin
ln -s /opt/python/cp27-cp27m/bin/python2.7 rm -f python3.5
ln -s /opt/python/cp35-cp35m/bin/python3.5
ln -s /opt/python/cp36-cp36m/bin/python3.6 ln -s /opt/python/cp36-cp36m/bin/python3.6
ln -s /opt/python/cp37-cp37m/bin/python3.7 ln -s /opt/python/cp37-cp37m/bin/python3.7
ln -s /opt/python/cp38-cp38/bin/python3.8 ln -s /opt/python/cp38-cp38/bin/python3.8

View File

@@ -20,10 +20,12 @@ libc = "0.2"
human-panic = "1.0.1" human-panic = "1.0.1"
num-traits = "0.2.6" num-traits = "0.2.6"
serde_json = "1.0" serde_json = "1.0"
async-std = "1.6.0"
anyhow = "1.0.28" anyhow = "1.0.28"
thiserror = "1.0.14" thiserror = "1.0.14"
[features] [features]
default = ["vendored", "nightly"] default = ["vendored"]
vendored = ["deltachat/vendored"] vendored = ["deltachat/vendored"]
nightly = ["deltachat/nightly"] nightly = ["deltachat/nightly"]

View File

@@ -19,6 +19,8 @@ typedef struct _dc_msg dc_msg_t;
typedef struct _dc_contact dc_contact_t; typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t; typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t; typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
/** /**
@@ -30,63 +32,40 @@ typedef struct _dc_provider dc_provider_t;
* *
* Let's start. * Let's start.
* *
* First of all, you have to **define an event-handler-function** * First of all, you have to **create a context object**
* that is called by the library on specific events * bound to a database.
* (eg. when the configuration is done or when fresh messages arrive). * The database is a normal sqlite-file and is created as needed:
* With this function you can create a Delta Chat context then:
* *
* ~~~ * ~~~
* #include <deltachat.h> * dc_context_t* context = dc_context_new(NULL, "example.db", NULL);
* ~~~
* *
* uintptr_t event_handler_func(dc_context_t* context, int event, * After that, make sure, you can **receive events from the context**.
* uintptr_t data1, uintptr_t data2) * For that purpose, create an event emitter you can ask for events.
* If there are no event, the emitter will wait until there is one,
* so, in many situations you will do this in a thread:
*
* ~~~
* void* event_handler(void* context)
* { * {
* return 0; * dc_event_emitter_t* emitter = dc_get_event_emitter(context);
* } * dc_event_t* event;
* * while ((event = dc_get_next_event(emitter)) != NULL) {
* dc_context_t* context = dc_context_new(event_handler_func, NULL, NULL); * // use the event as needed, eg. dc_event_get_id() returns the type.
* ~~~ * // once you're done, unref the event to avoid memory leakage:
* * dc_event_unref(event);
* After that, you should make sure,
* sending and receiving jobs are processed as needed.
* For this purpose, you have to **create two threads:**
*
* ~~~
* #include <pthread.h>
*
* void* imap_thread_func(void* context)
* {
* while (true) {
* dc_perform_imap_jobs(context);
* dc_perform_imap_fetch(context);
* dc_perform_imap_idle(context);
* } * }
* dc_event_emitter_unref(emitter);
* } * }
* *
* void* smtp_thread_func(void* context) * static pthread_t event_thread;
* { * pthread_create(&event_thread, NULL, event_handler, context);
* while (true) {
* dc_perform_smtp_jobs(context);
* dc_perform_smtp_idle(context);
* }
* }
*
* static pthread_t imap_thread, smtp_thread;
* pthread_create(&imap_thread, NULL, imap_thread_func, context);
* pthread_create(&smtp_thread, NULL, smtp_thread_func, context);
* ~~~ * ~~~
* *
* The example above uses "pthreads", * The example above uses "pthreads",
* however, you can also use anything else for thread handling. * however, you can also use anything else for thread handling.
* All deltachat-core-functions, unless stated otherwise, are thread-safe. * All deltachat-core-functions, unless stated otherwise, are thread-safe.
* *
* After that you can **define and open a database.**
* The database is a normal sqlite-file and is created as needed:
*
* ~~~
* dc_open(context, "example.db", NULL);
* ~~~
*
* Now you can **configure the context:** * Now you can **configure the context:**
* *
* ~~~ * ~~~
@@ -96,15 +75,22 @@ typedef struct _dc_provider dc_provider_t;
* dc_configure(context); * dc_configure(context);
* ~~~ * ~~~
* *
* dc_configure() returns immediately, the configuration itself may take a while * dc_configure() returns immediately,
* and is done by a job in the imap-thread you've defined above. * the configuration itself runs in background and may take a while.
* Once done, the #DC_EVENT_CONFIGURE_PROGRESS reports success * Once done, the #DC_EVENT_CONFIGURE_PROGRESS reports success
* to the event_handler_func() that is also defined above. * to the event_handler() you've defined above.
* *
* The configuration result is saved in the database, * The configuration result is saved in the database,
* on subsequent starts it is not needed to call dc_configure() * on subsequent starts it is not needed to call dc_configure()
* (you can check this using dc_is_configured()). * (you can check this using dc_is_configured()).
* *
* On a successfully configured context,
* you can finally **connect to the servers:**
*
* ~~~
* dc_start_io(context);
* ~~~
*
* Now you can **send the first message:** * Now you can **send the first message:**
* *
* ~~~ * ~~~
@@ -116,11 +102,11 @@ typedef struct _dc_provider dc_provider_t;
* ~~~ * ~~~
* *
* dc_send_text_msg() returns immediately; * dc_send_text_msg() returns immediately;
* the sending itself is done by a job in the smtp-thread you've defined above. * the sending itself is done in the background.
* If you check the testing address (bob) * If you check the testing address (bob)
* and you should have received a normal email. * and you should have received a normal email.
* Answer this email in any email program with "Got it!" * Answer this email in any email program with "Got it!"
* and the imap-thread you've create above will **receive the message**. * and the IO you started above will **receive the message**.
* *
* You can then **list all messages** of a chat as follow: * You can then **list all messages** of a chat as follow:
* *
@@ -164,19 +150,6 @@ typedef struct _dc_provider dc_provider_t;
* - The issue-tracker for the core library is here: * - The issue-tracker for the core library is here:
* <https://github.com/deltachat/deltachat-core-rust/issues> * <https://github.com/deltachat/deltachat-core-rust/issues>
* *
* The following points are important mainly
* for the authors of the library itself:
*
* - For indentation, use tabs.
* Alignments that are not placed at the beginning of a line
* should be done with spaces.
*
* - For padding between functions,
* classes etc. use 2 empty lines
*
* - Source files are encoded as UTF-8 with Unix line endings
* (a simple `LF`, `0x0A` or `\n`)
*
* If you need further assistance, * If you need further assistance,
* please do not hesitate to contact us * please do not hesitate to contact us
* through the channels shown at https://delta.chat/en/contribute * through the channels shown at https://delta.chat/en/contribute
@@ -199,20 +172,6 @@ typedef struct _dc_provider dc_provider_t;
* settings. * settings.
*/ */
/**
* Callback function that should be given to dc_context_new().
*
* @memberof dc_context_t
* @param context The context object as returned by dc_context_new().
* @param event one of the @ref DC_EVENT constants
* @param data1 depends on the event parameter
* @param data2 depends on the event parameter
* @return events do not expect a return value, just always return 0
*/
typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t data1, uintptr_t data2);
// create/open/config/information // create/open/config/information
/** /**
@@ -220,18 +179,6 @@ typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t
* opened, connected and mails are fetched. * opened, connected and mails are fetched.
* *
* @memberof dc_context_t * @memberof dc_context_t
* @param cb a callback function that is called for events (update,
* state changes etc.) and to get some information from the client (eg. translation
* for a given string).
* See @ref DC_EVENT for a list of possible events that may be passed to the callback.
* - The callback MAY be called from _any_ thread, not only the main/GUI thread!
* - The callback MUST NOT call any dc_* and related functions unless stated
* otherwise!
* - The callback SHOULD return _fast_, for GUI updates etc. you should
* post yourself an asynchronous message to your GUI thread, if needed.
* - events do not expect a return value, just always return 0.
* @param userdata can be used by the client for any purpuse. He finds it
* later in dc_get_userdata().
* @param os_name is only for decorative use * @param os_name is only for decorative use
* and is shown eg. in the `X-Mailer:` header * and is shown eg. in the `X-Mailer:` header
* in the form "Delta Chat Core <version>/<os_name>". * in the form "Delta Chat Core <version>/<os_name>".
@@ -239,11 +186,16 @@ typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t
* the used environment and/or the version here. * the used environment and/or the version here.
* It is okay to give NULL, in this case `X-Mailer:` header * It is okay to give NULL, in this case `X-Mailer:` header
* is set to "Delta Chat Core <version>". * is set to "Delta Chat Core <version>".
* @param dbfile The file to use to store the database,
* something like `~/file` won't work, use absolute paths.
* @param blobdir A directory to store the blobs in; a trailing slash is not needed.
* If you pass NULL or the empty string, deltachat-core creates a directory
* beside _dbfile_ with the same name and the suffix `-blobs`.
* @return A context object with some public members. * @return A context object with some public members.
* The object must be passed to the other context functions * The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage. * 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 (const char* os_name, const char* dbfile, const char* blobdir);
/** /**
@@ -262,56 +214,21 @@ void dc_context_unref (dc_context_t* context);
/** /**
* Get user data associated with a context object. * Create the event emitter that is used to receive events.
* The library will emit various @ref DC_EVENT events as "new message", "message read" etc.
* To get these events, you have to create an event emitter using this function
* and call dc_get_next_event() on the emitter.
* *
* @memberof dc_context_t * @memberof dc_context_t
* @param context The context object as created by dc_context_new(). * @param context The context object as created by dc_context_new().
* @return User data, this is the second parameter given to dc_context_new(). * @return Returns the event emitter, NULL on errors.
*/ * Must be freed using dc_event_emitter_unref() after usage.
void* dc_get_userdata (dc_context_t* context);
/**
* Open context database. If the given file does not exist, it is
* created and can be set up using dc_set_config() afterwards.
* *
* @memberof dc_context_t * Note: Use only one event emitter per context.
* @param context The context object as created by dc_context_new(). * Having more than one event emitter running at the same time on the same context
* @param dbfile The file to use to store the database, something like `~/file` won't * will result in events randomly delivered to the one or to the other.
* work on all systems, if in doubt, use absolute paths.
* @param blobdir A directory to store the blobs in; a trailing slash is not needed.
* If you pass NULL or the empty string, deltachat-core creates a directory
* beside _dbfile_ with the same name and the suffix `-blobs`.
* @return 1 on success, 0 on failure
* eg. if the file is not writable
* or if there is already a database opened for the context.
*/ */
int dc_open (dc_context_t* context, const char* dbfile, const char* blobdir); dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
/**
* Close context database opened by dc_open().
* Before this, connections to SMTP and IMAP are closed; these connections
* are started automatically as needed eg. by sending for fetching messages.
* This function is also implicitly called by dc_context_unref().
* Multiple calls to this functions are okay, the function takes care not
* to free objects twice.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @return None.
*/
void dc_close (dc_context_t* context);
/**
* Check if the context database is open.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @return 0=context is not open, 1=context is open.
*/
int dc_is_open (const dc_context_t* context);
/** /**
@@ -513,9 +430,7 @@ char* dc_get_oauth2_url (dc_context_t* context, const char*
/** /**
* Configure a context. * Configure a context.
* For this purpose, the function creates a job * While configuration IO must not be started, if needed stop IO using dc_stop_io() first.
* that is executed in the IMAP-thread then;
* this requires to call dc_perform_imap_jobs() regularly.
* If the context is already configured, * If the context is already configured,
* this function will try to change the configuration. * this function will try to change the configuration.
* *
@@ -577,311 +492,40 @@ void dc_configure (dc_context_t* context);
* @return 1=context is configured and can be used; * @return 1=context is configured and can be used;
* 0=context is not configured and a configuration by dc_configure() is required. * 0=context is not configured and a configuration by dc_configure() is required.
*/ */
int dc_is_configured (const dc_context_t* context); int dc_is_configured (const dc_context_t* context);
/** /**
* Execute pending imap-jobs. * Start job and IMAP/SMTP tasks.
* This function and dc_perform_imap_fetch() and dc_perform_imap_idle() * If IO is already running, nothing happens.
* must be called from the same thread, typically in a loop. * To check the current IO state, use dc_is_io_running().
*
* 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 * @memberof dc_context_t
* @param context The context as created by dc_context_new(). * @param context The context object as created by dc_context_new().
* @return None. * @return None
*/ */
void dc_perform_imap_jobs (dc_context_t* context); void dc_start_io (dc_context_t* context);
/** /**
* Fetch new messages, if any. * Check if IO (SMTP/IMAP/Jobs) has been started.
* 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 * @memberof dc_context_t
* @param context The context as created by dc_context_new(). * @param context The context object as created by dc_context_new().
* @return None. * @return 1=IO is running;
* 0=IO is not running.
*/ */
void dc_perform_imap_fetch (dc_context_t* context); int dc_is_io_running(const dc_context_t* context);
/** /**
* Wait for messages or jobs. * Stop job and IMAP/SMTP tasks and return when they are finished.
* This function and dc_perform_imap_jobs() and dc_perform_imap_fetch() must be called from the same thread, * If IO is not running, nothing happens.
* typically in a loop. * To check the current IO state, use dc_is_io_running().
*
* You should call this function directly after calling dc_perform_imap_fetch().
*
* See dc_perform_imap_jobs() for an example.
* *
* @memberof dc_context_t * @memberof dc_context_t
* @param context The context as created by dc_context_new(). * @param context The context object as created by dc_context_new().
* @return None. * @return None
*/ */
void dc_perform_imap_idle (dc_context_t* context); void dc_stop_io(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);
/** /**
* This function can be called whenever there is a hint * This function can be called whenever there is a hint
@@ -895,6 +539,7 @@ void dc_interrupt_smtp_idle (dc_context_t* context);
void dc_maybe_network (dc_context_t* context); void dc_maybe_network (dc_context_t* context);
/** /**
* Save a keypair as the default keys for the user. * Save a keypair as the default keys for the user.
* *
@@ -1121,6 +766,23 @@ uint32_t dc_prepare_msg (dc_context_t* context, uint32_t ch
*/ */
uint32_t dc_send_msg (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg); uint32_t dc_send_msg (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg);
/**
* Send a message defined by a dc_msg_t object to a chat, synchronously.
* This bypasses the IO scheduler and creates its own SMTP connection. Which means
* this is useful when the scheduler is not running.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id Chat ID to send the message to.
* If dc_prepare_msg() was called before, this parameter can be 0.
* @param msg Message object to send to the chat defined by the chat ID.
* On succcess, msg_id of the object is set up,
* The function does not take ownership of the object,
* so you have to free it using dc_msg_unref() as usual.
* @return The ID of the message that is about to be sent. 0 in case of errors.
*/
uint32_t dc_send_msg_sync (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg);
/** /**
* Send a simple text message a given chat. * Send a simple text message a given chat.
@@ -1976,9 +1638,6 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
/** /**
* Import/export things. * Import/export things.
* For this purpose, the function creates a job that is executed in the IMAP-thread then;
* this requires to call dc_perform_imap_jobs() regularly.
*
* What to do is defined by the _what_ parameter which may be one of the following: * What to do is defined by the _what_ parameter which may be one of the following:
* *
* - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`. * - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`.
@@ -2751,7 +2410,6 @@ dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
* last-message-date: * last-message-date:
* avatar-path: path-to-blobfile * avatar-path: path-to-blobfile
* is_verified: yes/no * is_verified: yes/no
* @return a utf8-encoded json string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned. * @return a utf8-encoded json string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
*/ */
char* dc_chat_get_info_json (dc_context_t* context, size_t chat_id); char* dc_chat_get_info_json (dc_context_t* context, size_t chat_id);
@@ -4160,11 +3818,122 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_EMPTY_INBOX 0x02 // Deprecated, flag for dc_empty_server(): Clear all INBOX messages #define DC_EMPTY_INBOX 0x02 // Deprecated, flag for dc_empty_server(): Clear all INBOX messages
/**
* @class dc_event_emitter_t
*
* Opaque object that is used to get events.
* You can get an event emitter from a context using dc_get_event_emitter().
*/
/**
* Get the next event from an event emitter object.
*
* @memberof dc_event_emitter_t
* @param emitter Event emitter object as returned from dc_get_event_emitter().
* @return An event as an dc_event_t object.
* You can query the event for information using dc_event_get_id(), dc_event_get_data1_int() and so on;
* if you are done with the event, you have to free the event using dc_event_unref().
* If NULL is returned, the context belonging to the event emitter is unref'd and the no more events will come;
* in this case, free the event emitter using dc_event_emitter_unref().
*/
dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
/**
* Free an event emitter object.
*
* @memberof dc_event_emitter_t
* @param emitter Event emitter object as returned from dc_get_event_emitter().
* If NULL is given, nothing is done and an error is logged.
* @return None.
*/
void dc_event_emitter_unref(dc_event_emitter_t* emitter);
/**
* @class dc_event_t
*
* Opaque object describing a single event.
* To get events, call dc_get_next_event() on an event emitter created by dc_get_event_emitter().
*/
/**
* Get the event-id from an event object.
* The event-id is one of the @ref DC_EVENT constants.
* There may be additional data belonging to an event,
* to get them, use dc_event_get_data1_int(), dc_event_get_data2_int() and dc_event_get_data2_str().
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return once of the @ref DC_EVENT constants.
* 0 on errors.
*/
int dc_event_get_id(dc_event_t* event);
/**
* Get a data associated with an event object.
* The meaning of the data depends on the event-id
* returned as @ref DC_EVENT constants by dc_event_get_id().
* See also dc_event_get_data2_int() and dc_event_get_data2_str().
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return "data1" as a signed integer, at least 32bit,
* the meaning depends on the event type associated with this event.
*/
int dc_event_get_data1_int(dc_event_t* event);
/**
* Get a data associated with an event object.
* The meaning of the data depends on the event-id
* returned as @ref DC_EVENT constants by dc_event_get_id().
* See also dc_event_get_data2_int() and dc_event_get_data2_str().
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return "data2" as a signed integer, at least 32bit,
* the meaning depends on the event type associated with this event.
*/
int dc_event_get_data2_int(dc_event_t* event);
/**
* Get a data associated with an event object.
* The meaning of the data depends on the event-id
* returned as @ref DC_EVENT constants by dc_event_get_id().
* See also dc_event_get_data1_int() and dc_event_get_data2_int().
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return "data2" as a string,
* the meaning depends on the event type associated with this event.
* Once you're done with the string, you have to unref it using dc_unref_str().
*/
char* dc_event_get_data2_str(dc_event_t* event);
/**
* Free memory used by an event object.
* If you forget to do this for an event, this will result in memory leakage.
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return None.
*/
void dc_event_unref(dc_event_t* event);
/** /**
* @defgroup DC_EVENT DC_EVENT * @defgroup DC_EVENT DC_EVENT
* *
* These constants are used as events * These constants are used as event-id
* reported to the callback given to dc_context_new(). * in events returned by dc_get_next_event().
*
* Events typically come with some additional data,
* use dc_event_get_data1_int(), dc_event_get_data2_int() and dc_event_get_data2_str() to read this data.
* The meaning of the data depends on the event.
* *
* @addtogroup DC_EVENT * @addtogroup DC_EVENT
* @{ * @{
@@ -4172,13 +3941,11 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/** /**
* The library-user may write an informational string to the log. * The library-user may write an informational string to the log.
* Passed to the callback given to dc_context_new().
* *
* This event should not be reported to the end-user using a popup or something like that. * This event should not be reported to the end-user using a popup or something like that.
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) Info string in english language. * @param data2 (char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_INFO 100 #define DC_EVENT_INFO 100
@@ -4187,8 +3954,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* Emitted when SMTP connection is established and login was successful. * Emitted when SMTP connection is established and login was successful.
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) Info string in english language. * @param data2 (char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_SMTP_CONNECTED 101 #define DC_EVENT_SMTP_CONNECTED 101
@@ -4197,8 +3963,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* Emitted when IMAP connection is established and login was successful. * Emitted when IMAP connection is established and login was successful.
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) Info string in english language. * @param data2 (char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_IMAP_CONNECTED 102 #define DC_EVENT_IMAP_CONNECTED 102
@@ -4206,8 +3971,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* Emitted when a message was successfully sent to the SMTP server. * Emitted when a message was successfully sent to the SMTP server.
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) Info string in english language. * @param data2 (char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_SMTP_MESSAGE_SENT 103 #define DC_EVENT_SMTP_MESSAGE_SENT 103
@@ -4215,8 +3979,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* Emitted when a message was successfully marked as deleted on the IMAP server. * Emitted when a message was successfully marked as deleted on the IMAP server.
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) Info string in english language. * @param data2 (char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_IMAP_MESSAGE_DELETED 104 #define DC_EVENT_IMAP_MESSAGE_DELETED 104
@@ -4224,8 +3987,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* Emitted when a message was successfully moved on IMAP. * Emitted when a message was successfully moved on IMAP.
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) Info string in english language. * @param data2 (char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_IMAP_MESSAGE_MOVED 105 #define DC_EVENT_IMAP_MESSAGE_MOVED 105
@@ -4233,8 +3995,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* Emitted when an IMAP folder was emptied. * Emitted when an IMAP folder was emptied.
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) folder name. * @param data2 (char*) Folder name.
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_IMAP_FOLDER_EMPTIED 106 #define DC_EVENT_IMAP_FOLDER_EMPTIED 106
@@ -4242,8 +4003,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* Emitted when a new blob file was successfully written * Emitted when a new blob file was successfully written
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) path name * @param data2 (char*) Path name
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_NEW_BLOB_FILE 150 #define DC_EVENT_NEW_BLOB_FILE 150
@@ -4251,27 +4011,23 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* Emitted when a blob file was successfully deleted * Emitted when a blob file was successfully deleted
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) path name * @param data2 (char*) Path name
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_DELETED_BLOB_FILE 151 #define DC_EVENT_DELETED_BLOB_FILE 151
/** /**
* The library-user should write a warning string to the log. * The library-user should write a warning string to the log.
* Passed to the callback given to dc_context_new().
* *
* This event should not be reported to the end-user using a popup or something like that. * This event should not be reported to the end-user using a popup or something like that.
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) Warning string in english language. * @param data2 (char*) Warning string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_WARNING 300 #define DC_EVENT_WARNING 300
/** /**
* The library-user should report an error to the end-user. * The library-user should report an error to the end-user.
* Passed to the callback given to dc_context_new().
* *
* As most things are asynchronous, things may go wrong at any time and the user * As most things are asynchronous, things may go wrong at any time and the user
* should not be disturbed by a dialog or so. Instead, use a bubble or so. * should not be disturbed by a dialog or so. Instead, use a bubble or so.
@@ -4283,10 +4039,9 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* in a messasge box then. * in a messasge box then.
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) Error string, always set, never NULL. * @param data2 (char*) Error string, always set, never NULL.
* Some error strings are taken from dc_set_stock_translation(), * Some error strings are taken from dc_set_stock_translation(),
* however, most error strings will be in english language. * however, most error strings will be in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_ERROR 400 #define DC_EVENT_ERROR 400
@@ -4308,8 +4063,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* *
* @param data1 (int) 1=first/new network error, should be reported the user; * @param data1 (int) 1=first/new network error, should be reported the user;
* 0=subsequent network error, should be logged only * 0=subsequent network error, should be logged only
* @param data2 (const char*) Error string, always set, never NULL. * @param data2 (char*) Error string, always set, never NULL.
* Must not be unref'd or modified and is valid only until the callback returns.
*/ */
#define DC_EVENT_ERROR_NETWORK 401 #define DC_EVENT_ERROR_NETWORK 401
@@ -4322,9 +4076,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* dc_send_text_msg() or another sending function. * dc_send_text_msg() or another sending function.
* *
* @param data1 0 * @param data1 0
* @param data2 (const char*) Info string in english language. * @param data2 (char*) Info string in english language.
* Must not be unref'd or modified
* and is valid only until the callback returns.
*/ */
#define DC_EVENT_ERROR_SELF_NOT_IN_GROUP 410 #define DC_EVENT_ERROR_SELF_NOT_IN_GROUP 410
@@ -4442,9 +4194,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* A typical purpose for a handler of this event may be to make the file public to some system * A typical purpose for a handler of this event may be to make the file public to some system
* services. * services.
* *
* @param data1 (const char*) Path and file name. * @param data1 0
* Must not be unref'd or modified and is valid only until the callback returns. * @param data2 (char*) Path and file name.
* @param data2 0
*/ */
#define DC_EVENT_IMEX_FILE_WRITTEN 2052 #define DC_EVENT_IMEX_FILE_WRITTEN 2052
@@ -4491,8 +4242,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_ERROR_SEE_STRING 0 // not used anymore #define DC_ERROR_SEE_STRING 0 // not used anymore
#define DC_ERROR_SELF_NOT_IN_GROUP 1 // not used anymore #define DC_ERROR_SELF_NOT_IN_GROUP 1 // not used anymore
#define DC_STR_SELFNOTINGRP 21 // not used anymore #define DC_STR_SELFNOTINGRP 21 // not used anymore
#define DC_EVENT_DATA1_IS_STRING(e) ((e)==DC_EVENT_IMEX_FILE_WRITTEN || (e)==DC_EVENT_FILE_COPIED) #define DC_EVENT_DATA1_IS_STRING(e) 0 // not used anymore
#define DC_EVENT_DATA2_IS_STRING(e) ((e)>=100 && (e)<=499) #define DC_EVENT_DATA2_IS_STRING(e) ((e)==DC_EVENT_IMEX_FILE_WRITTEN || ((e)>=100 && (e)<=499))
#define DC_EVENT_RETURNS_INT(e) ((e)==DC_EVENT_IS_OFFLINE) // not used anymore #define DC_EVENT_RETURNS_INT(e) ((e)==DC_EVENT_IS_OFFLINE) // not used anymore
#define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore #define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore
#define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore #define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use anyhow::{bail, ensure}; use anyhow::{bail, ensure};
use async_std::path::Path;
use deltachat::chat::{self, Chat, ChatId, ChatVisibility}; use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
use deltachat::chatlist::*; use deltachat::chatlist::*;
use deltachat::constants::*; use deltachat::constants::*;
@@ -11,7 +11,6 @@ use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*; use deltachat::dc_tools::*;
use deltachat::error::Error; use deltachat::error::Error;
use deltachat::imex::*; use deltachat::imex::*;
use deltachat::job::*;
use deltachat::location; use deltachat::location;
use deltachat::lot::LotState; use deltachat::lot::LotState;
use deltachat::message::{self, Message, MessageState, MsgId}; use deltachat::message::{self, Message, MessageState, MsgId};
@@ -24,78 +23,79 @@ use deltachat::{config, provider};
/// Reset database tables. /// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call. /// Argument is a bitmask, executing single or multiple actions in one call.
/// e.g. bitmask 7 triggers actions definded with bits 1, 2 and 4. /// e.g. bitmask 7 triggers actions definded with bits 1, 2 and 4.
fn dc_reset_tables(context: &Context, bits: i32) -> i32 { async fn reset_tables(context: &Context, bits: i32) {
println!("Resetting tables ({})...", bits); println!("Resetting tables ({})...", bits);
if 0 != bits & 1 { if 0 != bits & 1 {
sql::execute(context, &context.sql, "DELETE FROM jobs;", params![]).unwrap(); context
.sql()
.execute("DELETE FROM jobs;", paramsv![])
.await
.unwrap();
println!("(1) Jobs reset."); println!("(1) Jobs reset.");
} }
if 0 != bits & 2 { if 0 != bits & 2 {
sql::execute( context
context, .sql()
&context.sql, .execute("DELETE FROM acpeerstates;", paramsv![])
"DELETE FROM acpeerstates;", .await
params![], .unwrap();
)
.unwrap();
println!("(2) Peerstates reset."); println!("(2) Peerstates reset.");
} }
if 0 != bits & 4 { if 0 != bits & 4 {
sql::execute(context, &context.sql, "DELETE FROM keypairs;", params![]).unwrap(); context
.sql()
.execute("DELETE FROM keypairs;", paramsv![])
.await
.unwrap();
println!("(4) Private keypairs reset."); println!("(4) Private keypairs reset.");
} }
if 0 != bits & 8 { if 0 != bits & 8 {
sql::execute( context
context, .sql()
&context.sql, .execute("DELETE FROM contacts WHERE id>9;", paramsv![])
"DELETE FROM contacts WHERE id>9;", .await
params![], .unwrap();
) context
.unwrap(); .sql()
sql::execute( .execute("DELETE FROM chats WHERE id>9;", paramsv![])
context, .await
&context.sql, .unwrap();
"DELETE FROM chats WHERE id>9;", context
params![], .sql()
) .execute("DELETE FROM chats_contacts;", paramsv![])
.unwrap(); .await
sql::execute( .unwrap();
context, context
&context.sql, .sql()
"DELETE FROM chats_contacts;", .execute("DELETE FROM msgs WHERE id>9;", paramsv![])
params![], .await
) .unwrap();
.unwrap(); context
sql::execute( .sql()
context, .execute(
&context.sql, "DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
"DELETE FROM msgs WHERE id>9;", paramsv![],
params![], )
) .await
.unwrap(); .unwrap();
sql::execute( context
context, .sql()
&context.sql, .execute("DELETE FROM leftgrps;", paramsv![])
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';", .await
params![], .unwrap();
)
.unwrap();
sql::execute(context, &context.sql, "DELETE FROM leftgrps;", params![]).unwrap();
println!("(8) Rest but server config reset."); println!("(8) Rest but server config reset.");
} }
context.call_cb(Event::MsgsChanged { context.emit_event(Event::MsgsChanged {
chat_id: ChatId::new(0), chat_id: ChatId::new(0),
msg_id: MsgId::new(0), msg_id: MsgId::new(0),
}); });
1
} }
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), anyhow::Error> { async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), anyhow::Error> {
let data = dc_read_file(context, filename)?; let data = dc_read_file(context, filename).await?;
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false) { if let Err(err) = dc_receive_imf(context, &data, "import", 0, false).await {
println!("dc_receive_imf errored: {:?}", err); println!("dc_receive_imf errored: {:?}", err);
} }
Ok(()) Ok(())
@@ -104,38 +104,29 @@ fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(),
/// Import a file to the database. /// Import a file to the database.
/// For testing, import a folder with eml-files, a single eml-file, e-mail plus public key and so on. /// For testing, import a folder with eml-files, a single eml-file, e-mail plus public key and so on.
/// For normal importing, use imex(). /// For normal importing, use imex().
/// async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
/// @private @memberof Context let mut read_cnt: usize = 0;
/// @param context The context as created by dc_context_new().
/// @param spec The file or directory to import. NULL for the last command.
/// @return 1=success, 0=error.
fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
if !context.sql.is_open() {
error!(context, "Import: Database not opened.");
return 0;
}
let mut read_cnt = 0;
let real_spec: String; let real_spec: String;
/* if `spec` is given, remember it for later usage; if it is not given, try to use the last one */ // if `spec` is given, remember it for later usage; if it is not given, try to use the last one
if let Some(spec) = spec { if let Some(spec) = spec {
real_spec = spec.to_string(); real_spec = spec.to_string();
context context
.sql .sql()
.set_raw_config(context, "import_spec", Some(&real_spec)) .set_raw_config(context, "import_spec", Some(&real_spec))
.await
.unwrap(); .unwrap();
} else { } else {
let rs = context.sql.get_raw_config(context, "import_spec"); let rs = context.sql().get_raw_config(context, "import_spec").await;
if rs.is_none() { if rs.is_none() {
error!(context, "Import: No file or folder given."); error!(context, "Import: No file or folder given.");
return 0; return false;
} }
real_spec = rs.unwrap(); real_spec = rs.unwrap();
} }
if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) { if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) {
if suffix == "eml" && dc_poke_eml_file(context, &real_spec).is_ok() { if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
read_cnt += 1 read_cnt += 1
} }
} else { } else {
@@ -144,7 +135,7 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
let dir = std::fs::read_dir(dir_name); let dir = std::fs::read_dir(dir_name);
if dir.is_err() { if dir.is_err() {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,); error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
return 0; return false;
} else { } else {
let dir = dir.unwrap(); let dir = dir.unwrap();
for entry in dir { for entry in dir {
@@ -157,7 +148,7 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
if name.ends_with(".eml") { if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", &real_spec, name); let path_plus_name = format!("{}/{}", &real_spec, name);
println!("Import: {}", path_plus_name); println!("Import: {}", path_plus_name);
if dc_poke_eml_file(context, path_plus_name).is_ok() { if poke_eml_file(context, path_plus_name).await.is_ok() {
read_cnt += 1 read_cnt += 1
} }
} }
@@ -166,16 +157,19 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
} }
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec); println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
if read_cnt > 0 { if read_cnt > 0 {
context.call_cb(Event::MsgsChanged { context.emit_event(Event::MsgsChanged {
chat_id: ChatId::new(0), chat_id: ChatId::new(0),
msg_id: MsgId::new(0), msg_id: MsgId::new(0),
}); });
} }
1 true
} }
fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) { async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let contact = Contact::get_by_id(context, msg.get_from_id()).expect("invalid contact"); let contact = Contact::get_by_id(context, msg.get_from_id())
.await
.expect("invalid contact");
let contact_name = contact.get_name(); let contact_name = contact.get_name();
let contact_id = contact.get_id(); let contact_id = contact.get_id();
@@ -218,7 +212,7 @@ fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
); );
} }
fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> { async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> {
let mut lines_out = 0; let mut lines_out = 0;
for &msg_id in msglist { for &msg_id in msglist {
if msg_id.is_daymarker() { if msg_id.is_daymarker() {
@@ -234,8 +228,8 @@ fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
); );
lines_out += 1 lines_out += 1
} }
let msg = Message::load_from_db(context, msg_id)?; let msg = Message::load_from_db(context, msg_id).await?;
log_msg(context, "", &msg); log_msg(context, "", &msg).await;
} }
} }
if lines_out > 0 { if lines_out > 0 {
@@ -246,18 +240,18 @@ fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn log_contactlist(context: &Context, contacts: &Vec<u32>) { async fn log_contactlist(context: &Context, contacts: &[u32]) {
let mut contacts = contacts.clone(); let mut contacts = contacts.to_vec();
if !contacts.contains(&1) { if !contacts.contains(&1) {
contacts.push(1); contacts.push(1);
} }
for contact_id in contacts { for contact_id in contacts {
let line; let line;
let mut line2 = "".to_string(); let mut line2 = "".to_string();
if let Ok(contact) = Contact::get_by_id(context, contact_id) { if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
let name = contact.get_name(); let name = contact.get_name();
let addr = contact.get_addr(); let addr = contact.get_addr();
let verified_state = contact.is_verified(context); let verified_state = contact.is_verified(context).await;
let verified_str = if VerifiedStatus::Unverified != verified_state { let verified_str = if VerifiedStatus::Unverified != verified_state {
if verified_state == VerifiedStatus::BidirectVerified { if verified_state == VerifiedStatus::BidirectVerified {
" √√" " √√"
@@ -281,7 +275,7 @@ fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
"addr unset" "addr unset"
} }
); );
let peerstate = Peerstate::from_addr(context, &context.sql, &addr); let peerstate = Peerstate::from_addr(context, &addr).await;
if peerstate.is_some() && contact_id != 1 as libc::c_uint { if peerstate.is_some() && contact_id != 1 as libc::c_uint {
line2 = format!( line2 = format!(
", prefer-encrypt={}", ", prefer-encrypt={}",
@@ -298,10 +292,9 @@ fn chat_prefix(chat: &Chat) -> &'static str {
chat.typ.into() chat.typ.into()
} }
pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> { pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Result<(), Error> {
let chat_id = *context.cmdline_sel_chat_id.read().unwrap();
let mut sel_chat = if !chat_id.is_unset() { let mut sel_chat = if !chat_id.is_unset() {
Chat::load_from_db(context, chat_id).ok() Chat::load_from_db(&context, *chat_id).await.ok()
} else { } else {
None None
}; };
@@ -342,7 +335,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
configure\n\ configure\n\
connect\n\ connect\n\
disconnect\n\ disconnect\n\
interrupt\n\
maybenetwork\n\ maybenetwork\n\
housekeeping\n\ housekeeping\n\
help imex (Import/Export)\n\ help imex (Import/Export)\n\
@@ -405,7 +397,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
=============================================" ============================================="
), ),
}, },
"initiate-key-transfer" => match initiate_key_transfer(context) { "initiate-key-transfer" => match initiate_key_transfer(&context).await {
Ok(setup_code) => println!( Ok(setup_code) => println!(
"Setup code for the transferred setup message: {}", "Setup code for the transferred setup message: {}",
setup_code, setup_code,
@@ -415,9 +407,9 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
"get-setupcodebegin" => { "get-setupcodebegin" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let msg_id: MsgId = MsgId::new(arg1.parse()?); let msg_id: MsgId = MsgId::new(arg1.parse()?);
let msg = Message::load_from_db(context, msg_id)?; let msg = Message::load_from_db(&context, msg_id).await?;
if msg.is_setupmessage() { if msg.is_setupmessage() {
let setupcodebegin = msg.get_setupcodebegin(context); let setupcodebegin = msg.get_setupcodebegin(&context).await;
println!( println!(
"The setup code for setup message {} starts with: {}", "The setup code for setup message {} starts with: {}",
msg_id, msg_id,
@@ -432,29 +424,29 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
!arg1.is_empty() && !arg2.is_empty(), !arg1.is_empty() && !arg2.is_empty(),
"Arguments <msg-id> <setup-code> expected" "Arguments <msg-id> <setup-code> expected"
); );
continue_key_transfer(context, MsgId::new(arg1.parse()?), &arg2)?; continue_key_transfer(&context, MsgId::new(arg1.parse()?), &arg2).await?;
} }
"has-backup" => { "has-backup" => {
has_backup(context, blobdir)?; has_backup(&context, blobdir).await?;
} }
"export-backup" => { "export-backup" => {
imex(context, ImexMode::ExportBackup, Some(blobdir)); imex(&context, ImexMode::ExportBackup, Some(blobdir)).await?;
} }
"import-backup" => { "import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing."); ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
imex(context, ImexMode::ImportBackup, Some(arg1)); imex(&context, ImexMode::ImportBackup, Some(arg1)).await?;
} }
"export-keys" => { "export-keys" => {
imex(context, ImexMode::ExportSelfKeys, Some(blobdir)); imex(&context, ImexMode::ExportSelfKeys, Some(blobdir)).await?;
} }
"import-keys" => { "import-keys" => {
imex(context, ImexMode::ImportSelfKeys, Some(blobdir)); imex(&context, ImexMode::ImportSelfKeys, Some(blobdir)).await?;
} }
"export-setup" => { "export-setup" => {
let setup_code = create_setup_code(context); let setup_code = create_setup_code(&context);
let file_name = blobdir.join("autocrypt-setup-message.html"); let file_name = blobdir.join("autocrypt-setup-message.html");
let file_content = render_setup_file(context, &setup_code)?; let file_content = render_setup_file(&context, &setup_code).await?;
std::fs::write(&file_name, file_content)?; async_std::fs::write(&file_name, file_content).await?;
println!( println!(
"Setup message written to: {}\nSetup code: {}", "Setup message written to: {}\nSetup code: {}",
file_name.display(), file_name.display(),
@@ -462,49 +454,47 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
); );
} }
"poke" => { "poke" => {
ensure!(0 != poke_spec(context, Some(arg1)), "Poke failed"); ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
} }
"reset" => { "reset" => {
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config"); ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
let bits: i32 = arg1.parse()?; let bits: i32 = arg1.parse()?;
ensure!(bits < 16, "<bits> must be lower than 16."); ensure!(bits < 16, "<bits> must be lower than 16.");
ensure!(0 != dc_reset_tables(context, bits), "Reset failed"); reset_tables(&context, bits).await;
} }
"stop" => { "stop" => {
context.stop_ongoing(); context.stop_ongoing().await;
} }
"set" => { "set" => {
ensure!(!arg1.is_empty(), "Argument <key> missing."); ensure!(!arg1.is_empty(), "Argument <key> missing.");
let key = config::Config::from_str(&arg1)?; let key = config::Config::from_str(&arg1)?;
let value = if arg2.is_empty() { None } else { Some(arg2) }; let value = if arg2.is_empty() { None } else { Some(arg2) };
context.set_config(key, value)?; context.set_config(key, value).await?;
} }
"get" => { "get" => {
ensure!(!arg1.is_empty(), "Argument <key> missing."); ensure!(!arg1.is_empty(), "Argument <key> missing.");
let key = config::Config::from_str(&arg1)?; let key = config::Config::from_str(&arg1)?;
let val = context.get_config(key); let val = context.get_config(key).await;
println!("{}={:?}", key, val); println!("{}={:?}", key, val);
} }
"info" => { "info" => {
println!("{:#?}", context.get_info()); println!("{:#?}", context.get_info().await);
}
"interrupt" => {
interrupt_inbox_idle(context);
} }
"maybenetwork" => { "maybenetwork" => {
maybe_network(context); context.maybe_network().await;
} }
"housekeeping" => { "housekeeping" => {
sql::housekeeping(context); sql::housekeeping(&context).await;
} }
"listchats" | "listarchived" | "chats" => { "listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 }; let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
let chatlist = Chatlist::try_load( let chatlist = Chatlist::try_load(
context, &context,
listflags, listflags,
if arg1.is_empty() { None } else { Some(arg1) }, if arg1.is_empty() { None } else { Some(arg1) },
None, None,
)?; )
.await?;
let cnt = chatlist.len(); let cnt = chatlist.len();
if cnt > 0 { if cnt > 0 {
@@ -513,20 +503,20 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
); );
for i in (0..cnt).rev() { for i in (0..cnt).rev() {
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?; let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
println!( println!(
"{}#{}: {} [{} fresh] {}", "{}#{}: {} [{} fresh] {}",
chat_prefix(&chat), chat_prefix(&chat),
chat.get_id(), chat.get_id(),
chat.get_name(), chat.get_name(),
chat.get_id().get_fresh_msg_cnt(context), chat.get_id().get_fresh_msg_cnt(&context).await,
match chat.visibility { match chat.visibility {
ChatVisibility::Normal => "", ChatVisibility::Normal => "",
ChatVisibility::Archived => "📦", ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌", ChatVisibility::Pinned => "📌",
}, },
); );
let lot = chatlist.get_summary(context, i, Some(&chat)); let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
let statestr = if chat.visibility == ChatVisibility::Archived { let statestr = if chat.visibility == ChatVisibility::Archived {
" [Archived]" " [Archived]"
} else { } else {
@@ -559,7 +549,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
); );
} }
} }
if location::is_sending_locations_to_chat(context, ChatId::new(0)) { if location::is_sending_locations_to_chat(&context, ChatId::new(0)).await {
println!("Location streaming enabled."); println!("Location streaming enabled.");
} }
println!("{} chats", cnt); println!("{} chats", cnt);
@@ -569,21 +559,21 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
bail!("Argument [chat-id] is missing."); bail!("Argument [chat-id] is missing.");
} }
if !arg1.is_empty() { if !arg1.is_empty() {
let chat_id = ChatId::new(arg1.parse()?); let id = ChatId::new(arg1.parse()?);
println!("Selecting chat {}", chat_id); println!("Selecting chat {}", id);
sel_chat = Some(Chat::load_from_db(context, chat_id)?); sel_chat = Some(Chat::load_from_db(&context, id).await?);
*context.cmdline_sel_chat_id.write().unwrap() = chat_id; *chat_id = id;
} }
ensure!(sel_chat.is_some(), "Failed to select chat"); ensure!(sel_chat.is_some(), "Failed to select chat");
let sel_chat = sel_chat.as_ref().unwrap(); let sel_chat = sel_chat.as_ref().unwrap();
let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, None); let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await;
let members = chat::get_chat_contacts(context, sel_chat.id); let members = chat::get_chat_contacts(&context, sel_chat.id).await;
let subtitle = if sel_chat.is_device_talk() { let subtitle = if sel_chat.is_device_talk() {
"device-talk".to_string() "device-talk".to_string()
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() { } else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
let contact = Contact::get_by_id(context, members[0])?; let contact = Contact::get_by_id(&context, members[0]).await?;
contact.get_addr().to_string() contact.get_addr().to_string()
} else { } else {
format!("{} member(s)", members.len()) format!("{} member(s)", members.len())
@@ -599,7 +589,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
} else { } else {
"" ""
}, },
match sel_chat.get_profile_image(context) { match sel_chat.get_profile_image(&context).await {
Some(icon) => match icon.to_str() { Some(icon) => match icon.to_str() {
Some(icon) => format!(" Icon: {}", icon), Some(icon) => format!(" Icon: {}", icon),
_ => " Icon: Err".to_string(), _ => " Icon: Err".to_string(),
@@ -607,38 +597,42 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
_ => "".to_string(), _ => "".to_string(),
}, },
); );
log_msglist(context, &msglist)?; log_msglist(&context, &msglist).await?;
if let Some(draft) = sel_chat.get_id().get_draft(context)? { if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
log_msg(context, "Draft", &draft); log_msg(&context, "Draft", &draft).await;
} }
println!("{} messages.", sel_chat.get_id().get_msg_cnt(context)); println!(
chat::marknoticed_chat(context, sel_chat.get_id())?; "{} messages.",
sel_chat.get_id().get_msg_cnt(&context).await
);
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
} }
"createchat" => { "createchat" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing."); ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id: libc::c_int = arg1.parse()?; let contact_id: libc::c_int = arg1.parse()?;
let chat_id = chat::create_by_contact_id(context, contact_id as u32)?; let chat_id = chat::create_by_contact_id(&context, contact_id as u32).await?;
println!("Single#{} created successfully.", chat_id,); println!("Single#{} created successfully.", chat_id,);
} }
"createchatbymsg" => { "createchatbymsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing"); ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?); let msg_id = MsgId::new(arg1.parse()?);
let chat_id = chat::create_by_msg_id(context, msg_id)?; let chat_id = chat::create_by_msg_id(&context, msg_id).await?;
let chat = Chat::load_from_db(context, chat_id)?; let chat = Chat::load_from_db(&context, chat_id).await?;
println!("{}#{} created successfully.", chat_prefix(&chat), chat_id,); println!("{}#{} created successfully.", chat_prefix(&chat), chat_id,);
} }
"creategroup" => { "creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing."); ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id = chat::create_group_chat(context, VerifiedStatus::Unverified, arg1)?; let chat_id =
chat::create_group_chat(&context, VerifiedStatus::Unverified, arg1).await?;
println!("Group#{} created successfully.", chat_id); println!("Group#{} created successfully.", chat_id);
} }
"createverified" => { "createverified" => {
ensure!(!arg1.is_empty(), "Argument <name> missing."); ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id = chat::create_group_chat(context, VerifiedStatus::Verified, arg1)?; let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?;
println!("VerifiedGroup#{} created successfully.", chat_id); println!("VerifiedGroup#{} created successfully.", chat_id);
} }
@@ -648,10 +642,12 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
let contact_id_0: libc::c_int = arg1.parse()?; let contact_id_0: libc::c_int = arg1.parse()?;
if chat::add_contact_to_chat( if chat::add_contact_to_chat(
context, &context,
sel_chat.as_ref().unwrap().get_id(), sel_chat.as_ref().unwrap().get_id(),
contact_id_0 as u32, contact_id_0 as u32,
) { )
.await
{
println!("Contact added to chat."); println!("Contact added to chat.");
} else { } else {
bail!("Cannot add contact to chat."); bail!("Cannot add contact to chat.");
@@ -662,17 +658,18 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing."); ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_1: libc::c_int = arg1.parse()?; let contact_id_1: libc::c_int = arg1.parse()?;
chat::remove_contact_from_chat( chat::remove_contact_from_chat(
context, &context,
sel_chat.as_ref().unwrap().get_id(), sel_chat.as_ref().unwrap().get_id(),
contact_id_1 as u32, contact_id_1 as u32,
)?; )
.await?;
println!("Contact added to chat."); println!("Contact added to chat.");
} }
"groupname" => { "groupname" => {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "Argument <name> missing."); ensure!(!arg1.is_empty(), "Argument <name> missing.");
chat::set_chat_name(context, sel_chat.as_ref().unwrap().get_id(), arg1)?; chat::set_chat_name(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
println!("Chat name set"); println!("Chat name set");
} }
@@ -680,24 +677,27 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "Argument <image> missing."); ensure!(!arg1.is_empty(), "Argument <image> missing.");
chat::set_chat_profile_image(context, sel_chat.as_ref().unwrap().get_id(), arg1)?; chat::set_chat_profile_image(&context, sel_chat.as_ref().unwrap().get_id(), arg1)
.await?;
println!("Chat image set"); println!("Chat image set");
} }
"chatinfo" => { "chatinfo" => {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
let contacts = chat::get_chat_contacts(context, sel_chat.as_ref().unwrap().get_id()); let contacts =
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await;
println!("Memberlist:"); println!("Memberlist:");
log_contactlist(context, &contacts); log_contactlist(&context, &contacts).await;
println!( println!(
"{} contacts\nLocation streaming: {}", "{} contacts\nLocation streaming: {}",
contacts.len(), contacts.len(),
location::is_sending_locations_to_chat( location::is_sending_locations_to_chat(
context, &context,
sel_chat.as_ref().unwrap().get_id() sel_chat.as_ref().unwrap().get_id()
), )
.await,
); );
} }
"getlocations" => { "getlocations" => {
@@ -705,12 +705,13 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
let contact_id = arg1.parse().unwrap_or_default(); let contact_id = arg1.parse().unwrap_or_default();
let locations = location::get_range( let locations = location::get_range(
context, &context,
sel_chat.as_ref().unwrap().get_id(), sel_chat.as_ref().unwrap().get_id(),
contact_id, contact_id,
0, 0,
0, 0,
); )
.await;
let default_marker = "-".to_string(); let default_marker = "-".to_string();
for location in &locations { for location in &locations {
let marker = location.marker.as_ref().unwrap_or(&default_marker); let marker = location.marker.as_ref().unwrap_or(&default_marker);
@@ -736,7 +737,12 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
ensure!(!arg1.is_empty(), "No timeout given."); ensure!(!arg1.is_empty(), "No timeout given.");
let seconds = arg1.parse()?; let seconds = arg1.parse()?;
location::send_locations_to_chat(context, sel_chat.as_ref().unwrap().get_id(), seconds); location::send_locations_to_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
seconds,
)
.await;
println!( println!(
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.", "Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
sel_chat.as_ref().unwrap().get_id(), sel_chat.as_ref().unwrap().get_id(),
@@ -751,7 +757,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
let latitude = arg1.parse()?; let latitude = arg1.parse()?;
let longitude = arg2.parse()?; let longitude = arg2.parse()?;
let continue_streaming = location::set(context, latitude, longitude, 0.); let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
if continue_streaming { if continue_streaming {
println!("Success, streaming should be continued."); println!("Success, streaming should be continued.");
} else { } else {
@@ -759,7 +765,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
} }
} }
"dellocations" => { "dellocations" => {
location::delete_all(context)?; location::delete_all(&context).await?;
} }
"send" => { "send" => {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
@@ -767,11 +773,11 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
let msg = format!("{} {}", arg1, arg2); let msg = format!("{} {}", arg1, arg2);
chat::send_text_msg(context, sel_chat.as_ref().unwrap().get_id(), msg)?; chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
} }
"sendempty" => { "sendempty" => {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_text_msg(context, sel_chat.as_ref().unwrap().get_id(), "".into())?; chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
} }
"sendimage" | "sendfile" => { "sendimage" | "sendfile" => {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
@@ -786,7 +792,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
if !arg2.is_empty() { if !arg2.is_empty() {
msg.set_text(Some(arg2.to_string())); msg.set_text(Some(arg2.to_string()));
} }
chat::send_msg(context, sel_chat.as_ref().unwrap().get_id(), &mut msg)?; chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
} }
"listmsgs" => { "listmsgs" => {
ensure!(!arg1.is_empty(), "Argument <query> missing."); ensure!(!arg1.is_empty(), "Argument <query> missing.");
@@ -797,9 +803,9 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
ChatId::new(0) ChatId::new(0)
}; };
let msglist = context.search_msgs(chat, arg1); let msglist = context.search_msgs(chat, arg1).await;
log_msglist(context, &msglist)?; log_msglist(&context, &msglist).await?;
println!("{} messages.", msglist.len()); println!("{} messages.", msglist.len());
} }
"draft" => { "draft" => {
@@ -812,10 +818,16 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
.as_ref() .as_ref()
.unwrap() .unwrap()
.get_id() .get_id()
.set_draft(context, Some(&mut draft)); .set_draft(&context, Some(&mut draft))
.await;
println!("Draft saved."); println!("Draft saved.");
} else { } else {
sel_chat.as_ref().unwrap().get_id().set_draft(context, None); sel_chat
.as_ref()
.unwrap()
.get_id()
.set_draft(&context, None)
.await;
println!("Draft deleted."); println!("Draft deleted.");
} }
} }
@@ -826,21 +838,22 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
); );
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(arg1.to_string())); msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(context, None, Some(&mut msg))?; chat::add_device_msg(&context, None, Some(&mut msg)).await?;
} }
"updatedevicechats" => { "updatedevicechats" => {
context.update_device_chats()?; context.update_device_chats().await?;
} }
"listmedia" => { "listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
let images = chat::get_chat_media( let images = chat::get_chat_media(
context, &context,
sel_chat.as_ref().unwrap().get_id(), sel_chat.as_ref().unwrap().get_id(),
Viewtype::Image, Viewtype::Image,
Viewtype::Gif, Viewtype::Gif,
Viewtype::Video, Viewtype::Video,
); )
.await;
println!("{} images or videos: ", images.len()); println!("{} images or videos: ", images.len());
for (i, data) in images.iter().enumerate() { for (i, data) in images.iter().enumerate() {
if 0 == i { if 0 == i {
@@ -849,36 +862,38 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
print!(", {}", data); print!(", {}", data);
} }
} }
print!("\n"); println!();
} }
"archive" | "unarchive" | "pin" | "unpin" => { "archive" | "unarchive" | "pin" | "unpin" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing."); ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?); let chat_id = ChatId::new(arg1.parse()?);
chat_id.set_visibility( chat_id
context, .set_visibility(
match arg0 { &context,
"archive" => ChatVisibility::Archived, match arg0 {
"unarchive" | "unpin" => ChatVisibility::Normal, "archive" => ChatVisibility::Archived,
"pin" => ChatVisibility::Pinned, "unarchive" | "unpin" => ChatVisibility::Normal,
_ => panic!("Unexpected command (This should never happen)"), "pin" => ChatVisibility::Pinned,
}, _ => panic!("Unexpected command (This should never happen)"),
)?; },
)
.await?;
} }
"delchat" => { "delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing."); ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?); let chat_id = ChatId::new(arg1.parse()?);
chat_id.delete(context)?; chat_id.delete(&context).await?;
} }
"msginfo" => { "msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?); let id = MsgId::new(arg1.parse()?);
let res = message::get_msg_info(context, id); let res = message::get_msg_info(&context, id).await;
println!("{}", res); println!("{}", res);
} }
"listfresh" => { "listfresh" => {
let msglist = context.get_fresh_msgs(); let msglist = context.get_fresh_msgs().await;
log_msglist(context, &msglist)?; log_msglist(&context, &msglist).await?;
print!("{} fresh messages.", msglist.len()); print!("{} fresh messages.", msglist.len());
} }
"forward" => { "forward" => {
@@ -890,37 +905,38 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
let mut msg_ids = [MsgId::new(0); 1]; let mut msg_ids = [MsgId::new(0); 1];
let chat_id = ChatId::new(arg2.parse()?); let chat_id = ChatId::new(arg2.parse()?);
msg_ids[0] = MsgId::new(arg1.parse()?); msg_ids[0] = MsgId::new(arg1.parse()?);
chat::forward_msgs(context, &msg_ids, chat_id)?; chat::forward_msgs(&context, &msg_ids, chat_id).await?;
} }
"markseen" => { "markseen" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = [MsgId::new(0); 1]; let mut msg_ids = vec![MsgId::new(0)];
msg_ids[0] = MsgId::new(arg1.parse()?); msg_ids[0] = MsgId::new(arg1.parse()?);
message::markseen_msgs(context, &msg_ids); message::markseen_msgs(&context, msg_ids).await;
} }
"star" | "unstar" => { "star" | "unstar" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = [MsgId::new(0); 1]; let mut msg_ids = vec![MsgId::new(0); 1];
msg_ids[0] = MsgId::new(arg1.parse()?); msg_ids[0] = MsgId::new(arg1.parse()?);
message::star_msgs(context, &msg_ids, arg0 == "star"); message::star_msgs(&context, msg_ids, arg0 == "star").await;
} }
"delmsg" => { "delmsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut ids = [MsgId::new(0); 1]; let mut ids = [MsgId::new(0); 1];
ids[0] = MsgId::new(arg1.parse()?); ids[0] = MsgId::new(arg1.parse()?);
message::delete_msgs(context, &ids); message::delete_msgs(&context, &ids).await;
} }
"listcontacts" | "contacts" | "listverified" => { "listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all( let contacts = Contact::get_all(
context, &context,
if arg0 == "listverified" { if arg0 == "listverified" {
0x1 | 0x2 0x1 | 0x2
} else { } else {
0x2 0x2
}, },
Some(arg1), Some(arg1),
)?; )
log_contactlist(context, &contacts); .await?;
log_contactlist(&context, &contacts).await;
println!("{} contacts.", contacts.len()); println!("{} contacts.", contacts.len());
} }
"addcontact" => { "addcontact" => {
@@ -928,30 +944,30 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
if !arg2.is_empty() { if !arg2.is_empty() {
let book = format!("{}\n{}", arg1, arg2); let book = format!("{}\n{}", arg1, arg2);
Contact::add_address_book(context, book)?; Contact::add_address_book(&context, book).await?;
} else { } else {
Contact::create(context, "", arg1)?; Contact::create(&context, "", arg1).await?;
} }
} }
"contactinfo" => { "contactinfo" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing."); ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?; let contact_id = arg1.parse()?;
let contact = Contact::get_by_id(context, contact_id)?; let contact = Contact::get_by_id(&context, contact_id).await?;
let name_n_addr = contact.get_name_n_addr(); let name_n_addr = contact.get_name_n_addr();
let mut res = format!( let mut res = format!(
"Contact info for: {}:\nIcon: {}\n", "Contact info for: {}:\nIcon: {}\n",
name_n_addr, name_n_addr,
match contact.get_profile_image(context) { match contact.get_profile_image(&context).await {
Some(image) => image.to_str().unwrap().to_string(), Some(image) => image.to_str().unwrap().to_string(),
None => "NoIcon".to_string(), None => "NoIcon".to_string(),
} }
); );
res += &Contact::get_encrinfo(context, contact_id)?; res += &Contact::get_encrinfo(&context, contact_id).await?;
let chatlist = Chatlist::try_load(context, 0, None, Some(contact_id))?; let chatlist = Chatlist::try_load(&context, 0, None, Some(contact_id)).await?;
let chatlist_cnt = chatlist.len(); let chatlist_cnt = chatlist.len();
if chatlist_cnt > 0 { if chatlist_cnt > 0 {
res += &format!( res += &format!(
@@ -962,7 +978,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
if 0 != i { if 0 != i {
res += ", "; res += ", ";
} }
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?; let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
res += &format!("{}#{}", chat_prefix(&chat), chat.get_id()); res += &format!("{}#{}", chat_prefix(&chat), chat.get_id());
} }
} }
@@ -971,11 +987,11 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
} }
"delcontact" => { "delcontact" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing."); ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
Contact::delete(context, arg1.parse()?)?; Contact::delete(&context, arg1.parse()?).await?;
} }
"checkqr" => { "checkqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing."); ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let res = check_qr(context, arg1); let res = check_qr(&context, arg1).await;
println!( println!(
"state={}, id={}, text1={:?}, text2={:?}", "state={}, id={}, text1={:?}, text2={:?}",
res.get_state(), res.get_state(),
@@ -986,7 +1002,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
} }
"setqr" => { "setqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing."); ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(context, arg1) { match set_config_from_qr(&context, arg1).await {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"), Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => println!("Cannot set config from QR code: {:?}", err), Err(err) => println!("Cannot set config from QR code: {:?}", err),
} }
@@ -1014,7 +1030,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
// ensure!(!arg1.is_empty(), "Argument <id> missing."); // ensure!(!arg1.is_empty(), "Argument <id> missing.");
// let event = arg1.parse()?; // let event = arg1.parse()?;
// let event = Event::from_u32(event).ok_or(format_err!("Event::from_u32({})", event))?; // let event = Event::from_u32(event).ok_or(format_err!("Event::from_u32({})", event))?;
// let r = context.call_cb(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t); // let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
// println!( // println!(
// "Sending event {:?}({}), received value {}.", // "Sending event {:?}({}), received value {}.",
// event, event as usize, r as libc::c_int, // event, event as usize, r as libc::c_int,
@@ -1023,7 +1039,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
"fileinfo" => { "fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing."); ensure!(!arg1.is_empty(), "Argument <file> missing.");
if let Ok(buf) = dc_read_file(context, &arg1) { if let Ok(buf) = dc_read_file(&context, &arg1).await {
let (width, height) = dc_get_filemeta(&buf)?; let (width, height) = dc_get_filemeta(&buf)?;
println!("width={}, height={}", width, height); println!("width={}, height={}", width, height);
} else { } else {
@@ -1033,8 +1049,8 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
"estimatedeletion" => { "estimatedeletion" => {
ensure!(!arg1.is_empty(), "Argument <seconds> missing"); ensure!(!arg1.is_empty(), "Argument <seconds> missing");
let seconds = arg1.parse()?; let seconds = arg1.parse()?;
let device_cnt = message::estimate_deletion_cnt(context, false, seconds)?; let device_cnt = message::estimate_deletion_cnt(&context, false, seconds).await?;
let server_cnt = message::estimate_deletion_cnt(context, true, seconds)?; let server_cnt = message::estimate_deletion_cnt(&context, true, seconds).await?;
println!( println!(
"estimated count of messages older than {} seconds:\non device: {}\non server: {}", "estimated count of messages older than {} seconds:\non device: {}\non server: {}",
seconds, device_cnt, server_cnt seconds, device_cnt, server_cnt
@@ -1043,7 +1059,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
"emptyserver" => { "emptyserver" => {
ensure!(!arg1.is_empty(), "Argument <flags> missing"); ensure!(!arg1.is_empty(), "Argument <flags> missing");
message::dc_empty_server(context, arg1.parse()?); message::dc_empty_server(&context, arg1.parse()?).await;
} }
"" => (), "" => (),
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0), _ => bail!("Unknown command: \"{}\" type ? for help.", arg0),

View File

@@ -6,26 +6,21 @@
#[macro_use] #[macro_use]
extern crate deltachat; extern crate deltachat;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate rusqlite;
use std::borrow::Cow::{self, Borrowed, Owned}; use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write}; use std::io::{self, Write};
use std::path::Path;
use std::process::Command; use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use ansi_term::Color;
use anyhow::{bail, Error}; use anyhow::{bail, Error};
use async_std::path::Path;
use deltachat::chat::ChatId; use deltachat::chat::ChatId;
use deltachat::config; use deltachat::config;
use deltachat::context::*; use deltachat::context::*;
use deltachat::job::*;
use deltachat::oauth2::*; use deltachat::oauth2::*;
use deltachat::securejoin::*; use deltachat::securejoin::*;
use deltachat::Event; use deltachat::Event;
use log::{error, info, warn};
use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType; use rustyline::config::OutputStreamType;
use rustyline::error::ReadlineError; use rustyline::error::ReadlineError;
@@ -38,179 +33,83 @@ use rustyline::{
mod cmdline; mod cmdline;
use self::cmdline::*; use self::cmdline::*;
// Event Handler /// Event Handler
fn receive_event(event: Event) {
fn receive_event(_context: &Context, event: Event) { let yellow = Color::Yellow.normal();
match event { match event {
Event::Info(msg) => { Event::Info(msg) => {
/* do not show the event as this would fill the screen */ /* do not show the event as this would fill the screen */
println!("{}", msg); info!("{}", msg);
} }
Event::SmtpConnected(msg) => { Event::SmtpConnected(msg) => {
println!("[DC_EVENT_SMTP_CONNECTED] {}", msg); info!("[SMTP_CONNECTED] {}", msg);
} }
Event::ImapConnected(msg) => { Event::ImapConnected(msg) => {
println!("[DC_EVENT_IMAP_CONNECTED] {}", msg); info!("[IMAP_CONNECTED] {}", msg);
} }
Event::SmtpMessageSent(msg) => { Event::SmtpMessageSent(msg) => {
println!("[DC_EVENT_SMTP_MESSAGE_SENT] {}", msg); info!("[SMTP_MESSAGE_SENT] {}", msg);
} }
Event::Warning(msg) => { Event::Warning(msg) => {
println!("[Warning] {}", msg); warn!("{}", msg);
} }
Event::Error(msg) => { Event::Error(msg) => {
println!("\x1b[31m[DC_EVENT_ERROR] {}\x1b[0m", msg); error!("{}", msg);
} }
Event::ErrorNetwork(msg) => { Event::ErrorNetwork(msg) => {
println!("\x1b[31m[DC_EVENT_ERROR_NETWORK] msg={}\x1b[0m", msg); error!("[NETWORK] msg={}", msg);
} }
Event::ErrorSelfNotInGroup(msg) => { Event::ErrorSelfNotInGroup(msg) => {
println!("\x1b[31m[DC_EVENT_ERROR_SELF_NOT_IN_GROUP] {}\x1b[0m", msg); error!("[SELF_NOT_IN_GROUP] {}", msg);
} }
Event::MsgsChanged { chat_id, msg_id } => { Event::MsgsChanged { chat_id, msg_id } => {
print!( info!(
"\x1b[33m{{Received DC_EVENT_MSGS_CHANGED(chat_id={}, msg_id={})}}\n\x1b[0m", "{}",
chat_id, msg_id, yellow.paint(format!(
"Received MSGS_CHANGED(chat_id={}, msg_id={})",
chat_id, msg_id,
))
); );
} }
Event::ContactsChanged(_) => { Event::ContactsChanged(_) => {
print!("\x1b[33m{{Received DC_EVENT_CONTACTS_CHANGED()}}\n\x1b[0m"); info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
} }
Event::LocationChanged(contact) => { Event::LocationChanged(contact) => {
print!( info!(
"\x1b[33m{{Received DC_EVENT_LOCATION_CHANGED(contact={:?})}}\n\x1b[0m", "{}",
contact, yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
); );
} }
Event::ConfigureProgress(progress) => { Event::ConfigureProgress(progress) => {
print!( info!(
"\x1b[33m{{Received DC_EVENT_CONFIGURE_PROGRESS({} ‰)}}\n\x1b[0m", "{}",
progress, yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
); );
} }
Event::ImexProgress(progress) => { Event::ImexProgress(progress) => {
print!( info!(
"\x1b[33m{{Received DC_EVENT_IMEX_PROGRESS({} ‰)}}\n\x1b[0m", "{}",
progress, yellow.paint(format!("Received IMEX_PROGRESS({} ‰)", progress))
); );
} }
Event::ImexFileWritten(file) => { Event::ImexFileWritten(file) => {
print!( info!(
"\x1b[33m{{Received DC_EVENT_IMEX_FILE_WRITTEN({})}}\n\x1b[0m", "{}",
file.display() yellow.paint(format!("Received IMEX_FILE_WRITTEN({})", file.display()))
); );
} }
Event::ChatModified(chat) => { Event::ChatModified(chat) => {
print!( info!(
"\x1b[33m{{Received DC_EVENT_CHAT_MODIFIED({})}}\n\x1b[0m", "{}",
chat yellow.paint(format!("Received CHAT_MODIFIED({})", chat))
); );
} }
_ => { _ => {
print!("\x1b[33m{{Received {:?}}}\n\x1b[0m", event); info!("Received {:?}", 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 // === The main loop
struct DcHelper { struct DcHelper {
@@ -367,69 +266,81 @@ impl Highlighter for DcHelper {
impl Helper for DcHelper {} impl Helper for DcHelper {}
fn main_0(args: Vec<String>) -> Result<(), Error> { async fn start(args: Vec<String>) -> Result<(), Error> {
if args.len() < 2 { if args.len() < 2 {
println!("Error: Bad arguments, expected [db-name]."); println!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified"); bail!("No db-name specified");
} }
let context = Context::new( let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf()).await?;
Box::new(receive_event),
"CLI".into(), let events = context.get_event_emitter();
Path::new(&args[1]).to_path_buf(), async_std::task::spawn(async move {
)?; while let Some(event) = events.recv().await {
receive_event(event);
}
});
println!("Delta Chat Core is awaiting your commands."); println!("Delta Chat Core is awaiting your commands.");
let ctx = Arc::new(RwLock::new(context));
let config = Config::builder() let config = Config::builder()
.history_ignore_space(true) .history_ignore_space(true)
.completion_type(CompletionType::List) .completion_type(CompletionType::List)
.edit_mode(EditMode::Emacs) .edit_mode(EditMode::Emacs)
.output_stream(OutputStreamType::Stdout) .output_stream(OutputStreamType::Stdout)
.build(); .build();
let h = DcHelper { let mut selected_chat = ChatId::default();
completer: FilenameCompleter::new(), let (reader_s, reader_r) = async_std::sync::channel(100);
highlighter: MatchingBracketHighlighter::new(), let input_loop = async_std::task::spawn_blocking(move || {
hinter: HistoryHinter {}, let h = DcHelper {
}; completer: FilenameCompleter::new(),
let mut rl = Editor::with_config(config); highlighter: MatchingBracketHighlighter::new(),
rl.set_helper(Some(h)); hinter: HistoryHinter {},
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward); };
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward); let mut rl = Editor::with_config(config);
if rl.load_history(".dc-history.txt").is_err() { rl.set_helper(Some(h));
println!("No previous history."); rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
} rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
if rl.load_history(".dc-history.txt").is_err() {
println!("No previous history.");
}
loop { loop {
let p = "> "; let p = "> ";
let readline = rl.readline(&p); let readline = rl.readline(&p);
match readline {
Ok(line) => { match readline {
// TODO: ignore "set mail_pw" Ok(line) => {
rl.add_history_entry(line.as_str()); // TODO: ignore "set mail_pw"
let ctx = ctx.clone(); rl.add_history_entry(line.as_str());
match handle_cmd(line.trim(), ctx) { async_std::task::block_on(reader_s.send(line));
Ok(ExitResult::Continue) => {} }
Ok(ExitResult::Exit) => break, Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err(err) => println!("Error: {}", err), println!("Exiting...");
drop(reader_s);
break;
}
Err(err) => {
println!("Error: {}", err);
drop(reader_s);
break;
} }
} }
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { }
println!("Exiting...");
break; rl.save_history(".dc-history.txt")?;
} println!("history saved");
Err(err) => { Ok::<_, Error>(())
println!("Error: {}", err); });
break;
} while let Ok(line) = reader_r.recv().await {
match handle_cmd(line.trim(), context.clone(), &mut selected_chat).await {
Ok(ExitResult::Continue) => {}
Ok(ExitResult::Exit) => break,
Err(err) => println!("Error: {}", err),
} }
} }
rl.save_history(".dc-history.txt")?; context.stop_io().await;
println!("history saved"); input_loop.await?;
{
stop_threads(&ctx.read().unwrap());
}
Ok(()) Ok(())
} }
@@ -440,43 +351,29 @@ enum ExitResult {
Exit, Exit,
} }
fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, Error> { async fn handle_cmd(
line: &str,
ctx: Context,
selected_chat: &mut ChatId,
) -> Result<ExitResult, Error> {
let mut args = line.splitn(2, ' '); let mut args = line.splitn(2, ' ');
let arg0 = args.next().unwrap_or_default(); let arg0 = args.next().unwrap_or_default();
let arg1 = args.next().unwrap_or_default(); let arg1 = args.next().unwrap_or_default();
match arg0 { match arg0 {
"connect" => { "connect" => {
start_threads(ctx); ctx.start_io().await;
} }
"disconnect" => { "disconnect" => {
stop_threads(&ctx.read().unwrap()); ctx.stop_io().await;
}
"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());
}
} }
"configure" => { "configure" => {
start_threads(ctx.clone()); ctx.configure().await?;
ctx.read().unwrap().configure();
} }
"oauth2" => { "oauth2" => {
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) { if let Some(addr) = ctx.get_config(config::Config::Addr).await {
let oauth2_url = dc_get_oauth2_url( let oauth2_url =
&ctx.read().unwrap(), dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
&addr,
"chat.delta:/com.b44t.messenger",
);
if oauth2_url.is_none() { if oauth2_url.is_none() {
println!("OAuth2 not available for {}.", &addr); println!("OAuth2 not available for {}.", &addr);
} else { } else {
@@ -491,11 +388,10 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, Error
print!("\x1b[1;1H\x1b[2J"); print!("\x1b[1;1H\x1b[2J");
} }
"getqr" | "getbadqr" => { "getqr" | "getbadqr" => {
start_threads(ctx.clone()); ctx.start_io().await;
if let Some(mut qr) = dc_get_securejoin_qr( if let Some(mut qr) =
&ctx.read().unwrap(), dc_get_securejoin_qr(&ctx, ChatId::new(arg1.parse().unwrap_or_default())).await
ChatId::new(arg1.parse().unwrap_or_default()), {
) {
if !qr.is_empty() { if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 { if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000") qr.replace_range(12..22, "0000000000")
@@ -511,23 +407,23 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, Error
} }
} }
"joinqr" => { "joinqr" => {
start_threads(ctx.clone()); ctx.start_io().await;
if !arg0.is_empty() { if !arg0.is_empty() {
dc_join_securejoin(&ctx.read().unwrap(), arg1); dc_join_securejoin(&ctx, arg1).await;
} }
} }
"exit" | "quit" => return Ok(ExitResult::Exit), "exit" | "quit" => return Ok(ExitResult::Exit),
_ => dc_cmdline(&ctx.read().unwrap(), line)?, _ => cmdline(ctx.clone(), line, selected_chat).await?,
} }
Ok(ExitResult::Continue) Ok(ExitResult::Continue)
} }
pub fn main() -> Result<(), Error> { fn main() -> Result<(), Error> {
let _ = pretty_env_logger::try_init(); let _ = pretty_env_logger::try_init();
let args: Vec<String> = std::env::args().collect(); let args = std::env::args().collect();
main_0(args)?; async_std::task::block_on(async move { start(args).await })?;
Ok(()) Ok(())
} }

View File

@@ -1,7 +1,3 @@
extern crate deltachat;
use std::sync::{Arc, RwLock};
use std::{thread, time};
use tempfile::tempdir; use tempfile::tempdir;
use deltachat::chat; use deltachat::chat;
@@ -9,103 +5,96 @@ use deltachat::chatlist::*;
use deltachat::config; use deltachat::config;
use deltachat::contact::*; use deltachat::contact::*;
use deltachat::context::*; use deltachat::context::*;
use deltachat::job::{ use deltachat::message::Message;
perform_inbox_fetch, perform_inbox_idle, perform_inbox_jobs, perform_smtp_idle,
perform_smtp_jobs,
};
use deltachat::Event; use deltachat::Event;
fn cb(_ctx: &Context, event: Event) { fn cb(event: Event) {
print!("[{:?}]", event);
match event { match event {
Event::ConfigureProgress(progress) => { Event::ConfigureProgress(progress) => {
println!(" progress: {}", progress); log::info!("progress: {}", progress);
} }
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => { Event::Info(msg) => {
println!(" {}", msg); log::info!("{}", msg);
} }
_ => { Event::Warning(msg) => {
println!(); log::warn!("{}", msg);
}
Event::Error(msg) | Event::ErrorNetwork(msg) => {
log::error!("{}", msg);
}
event => {
log::info!("{:?}", event);
} }
} }
} }
fn main() { /// Run with `RUST_LOG=simple=info cargo run --release --example simple --features repl -- email pw`.
#[async_std::main]
async fn main() {
pretty_env_logger::try_init_timed().ok();
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite"); let dbfile = dir.path().join("db.sqlite");
println!("creating database {:?}", dbfile); log::info!("creating database {:?}", dbfile);
let ctx = let ctx = Context::new("FakeOs".into(), dbfile.into())
Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context"); .await
let running = Arc::new(RwLock::new(true)); .expect("Failed to create context");
let info = ctx.get_info(); let info = ctx.get_info().await;
let duration = time::Duration::from_millis(4000); log::info!("info: {:#?}", info);
println!("info: {:#?}", info);
let ctx = Arc::new(ctx); let events = ctx.get_event_emitter();
let ctx1 = ctx.clone(); let events_spawn = async_std::task::spawn(async move {
let r1 = running.clone(); while let Some(event) = events.recv().await {
let t1 = thread::spawn(move || { cb(event);
while *r1.read().unwrap() {
perform_inbox_jobs(&ctx1);
if *r1.read().unwrap() {
perform_inbox_fetch(&ctx1);
if *r1.read().unwrap() {
perform_inbox_idle(&ctx1);
}
}
} }
}); });
let ctx1 = ctx.clone(); log::info!("configuring");
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);
}
}
});
println!("configuring");
let args = std::env::args().collect::<Vec<String>>(); let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 2, "missing password"); assert_eq!(args.len(), 3, "requires email password");
let pw = args[1].clone(); let email = args[1].clone();
ctx.set_config(config::Config::Addr, Some("d@testrun.org")) let pw = args[2].clone();
ctx.set_config(config::Config::Addr, Some(&email))
.await
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw))
.await
.unwrap(); .unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
ctx.configure();
thread::sleep(duration); ctx.configure().await.unwrap();
println!("sending a message"); log::info!("------ RUN ------");
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap(); ctx.start_io().await;
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap(); log::info!("--- SENDING A MESSAGE ---");
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
println!("fetching chats.."); let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap(); .await
.unwrap();
let chat_id = chat::create_by_contact_id(&ctx, contact_id).await.unwrap();
for i in 0..chats.len() { for i in 0..1 {
let summary = chats.get_summary(&ctx, 0, None); log::info!("sending message {}", i);
let text1 = summary.get_text1(); chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {}nth message!", i))
let text2 = summary.get_text2(); .await
println!("chat: {} - {:?} - {:?}", i, text1, text2,); .unwrap();
} }
thread::sleep(duration); // wait for the message to be sent out
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
println!("stopping threads"); log::info!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
*running.write().unwrap() = false; for i in 0..chats.len() {
deltachat::job::interrupt_inbox_idle(&ctx); let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap())
deltachat::job::interrupt_smtp_idle(&ctx); .await
.unwrap();
log::info!("[{}] msg: {:?}", i, msg);
}
println!("joining"); log::info!("stopping");
t1.join().unwrap(); ctx.stop_io().await;
t2.join().unwrap(); log::info!("closing");
drop(ctx);
println!("closing"); events_spawn.await;
} }

View File

@@ -113,10 +113,10 @@ You may look at `examples <https://py.delta.chat/examples.html>`_.
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust .. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
Building manylinux1 based wheels Building manylinux based wheels
================================ ====================================
Building portable manylinux1 wheels which come with libdeltachat.so Building portable manylinux wheels which come with libdeltachat.so
can be done with docker-tooling. can be done with docker-tooling.
using docker pull / premade images using docker pull / premade images

View File

@@ -14,8 +14,11 @@ class EchoPlugin:
# unconditionally accept the chat # unconditionally accept the chat
message.accept_sender_contact() message.accept_sender_contact()
addr = message.get_sender_contact().addr addr = message.get_sender_contact().addr
text = message.text if message.is_system_message():
message.chat.send_text("echoing from {}:\n{}".format(addr, text)) message.chat.send_text("echoing system message from {}:\n{}".format(addr, message))
else:
text = message.text
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
@account_hookimpl @account_hookimpl
def ac_message_delivered(self, message): def ac_message_delivered(self, message):

View File

@@ -3,7 +3,7 @@ import pytest
import py import py
import echo_and_quit import echo_and_quit
import group_tracking import group_tracking
from deltachat.eventlogger import FFIEventLogger from deltachat.events import FFIEventLogger
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
@@ -17,16 +17,23 @@ def datadir():
pytest.skip('test-data directory not found') pytest.skip('test-data directory not found')
def test_echo_quit_plugin(acfactory): def test_echo_quit_plugin(acfactory, lp):
lp.sec("creating one echo_and_quit bot")
botproc = acfactory.run_bot_process(echo_and_quit) botproc = acfactory.run_bot_process(echo_and_quit)
lp.sec("creating a temp account to contact the bot")
ac1 = acfactory.get_one_online_account() ac1 = acfactory.get_one_online_account()
lp.sec("sending a message to the bot")
bot_contact = ac1.create_contact(botproc.addr) bot_contact = ac1.create_contact(botproc.addr)
ch1 = ac1.create_chat_by_contact(bot_contact) ch1 = ac1.create_chat_by_contact(bot_contact)
ch1.send_text("hello") ch1.send_text("hello")
lp.sec("waiting for the bot-reply to arrive")
reply = ac1._evtracker.wait_next_incoming_message() reply = ac1._evtracker.wait_next_incoming_message()
assert "hello" in reply.text assert "hello" in reply.text
assert reply.chat == ch1 assert reply.chat == ch1
lp.sec("send quit sequence")
ch1.send_text("/quit") ch1.send_text("/quit")
botproc.wait() botproc.wait()

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_stop_io(ctx)

View File

@@ -19,6 +19,7 @@ if __name__ == "__main__":
cmd = ["cargo", "build", "-p", "deltachat_ffi"] cmd = ["cargo", "build", "-p", "deltachat_ffi"]
if target == 'release': if target == 'release':
cmd.append("--release") cmd.append("--release")
print("running:", " ".join(cmd))
subprocess.check_call(cmd) subprocess.check_call(cmd)
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True) subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)

View File

@@ -1,13 +1,13 @@
import sys import sys
from . import capi, const, hookspec from . import capi, const, hookspec # noqa
from .capi import ffi from .capi import ffi # noqa
from .account import Account # noqa from .account import Account # noqa
from .message import Message # noqa from .message import Message # noqa
from .contact import Contact # noqa from .contact import Contact # noqa
from .chat import Chat # noqa from .chat import Chat # noqa
from .hookspec import account_hookimpl, global_hookimpl # noqa from .hookspec import account_hookimpl, global_hookimpl # noqa
from . import eventlogger from . import events
from pkg_resources import get_distribution, DistributionNotFound from pkg_resources import get_distribution, DistributionNotFound
try: try:
@@ -17,64 +17,6 @@ except DistributionNotFound:
__version__ = "0.0.0.dev0-unknown" __version__ = "0.0.0.dev0-unknown"
_DC_CALLBACK_MAP = {}
@capi.ffi.def_extern()
def py_dc_callback(ctx, evt, data1, data2):
"""The global event handler.
CFFI only allows us to set one global event handler, so this one
looks up the correct event handler for the given context.
"""
try:
callback = _DC_CALLBACK_MAP.get(ctx, lambda *a: 0)
except AttributeError:
# we are in a deep in GC-free/interpreter shutdown land
# nothing much better to do here than:
return 0
# the following code relates to the deltachat/_build.py's helper
# function which provides us signature info of an event call
evt_name = get_dc_event_name(evt)
event_sig_types = capi.lib.dc_get_event_signature_types(evt)
if data1 and event_sig_types & 1:
data1 = ffi.string(ffi.cast('char*', data1)).decode("utf8")
if data2 and event_sig_types & 2:
data2 = ffi.string(ffi.cast('char*', data2)).decode("utf8")
try:
if isinstance(data2, bytes):
data2 = data2.decode("utf8")
except UnicodeDecodeError:
# XXX ignoring the decode error is not quite correct but for now
# i don't want to hunt down encoding problems in the c lib
pass
try:
ret = callback(ctx, evt_name, data1, data2)
if ret is None:
ret = 0
assert isinstance(ret, int), repr(ret)
if event_sig_types & 4:
return ffi.cast('uintptr_t', ret)
elif event_sig_types & 8:
return ffi.cast('int', ret)
except: # noqa
raise
ret = 0
return ret
def set_context_callback(dc_context, func):
_DC_CALLBACK_MAP[dc_context] = func
def clear_context_callback(dc_context):
try:
_DC_CALLBACK_MAP.pop(dc_context, None)
except AttributeError:
pass
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}): def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
if not _DC_EVENTNAME_MAP: if not _DC_EVENTNAME_MAP:
for name, val in vars(const).items(): for name, val in vars(const).items():
@@ -97,7 +39,7 @@ def unregister_global_plugin(plugin):
gm.unregister(plugin) gm.unregister(plugin)
register_global_plugin(eventlogger) register_global_plugin(events)
def run_cmdline(argv=None, account_plugins=None): def run_cmdline(argv=None, account_plugins=None):
@@ -118,9 +60,13 @@ def run_cmdline(argv=None, account_plugins=None):
ac = Account(args.db) ac = Account(args.db)
if args.show_ffi: if args.show_ffi:
log = eventlogger.FFIEventLogger(ac, "bot") log = events.FFIEventLogger(ac, "bot")
ac.add_account_plugin(log) ac.add_account_plugin(log)
for plugin in account_plugins or []:
print("adding plugin", plugin)
ac.add_account_plugin(plugin)
if not ac.is_configured(): if not ac.is_configured():
assert args.email and args.password, ( assert args.email and args.password, (
"you must specify --email and --password once to configure this database/account" "you must specify --email and --password once to configure this database/account"
@@ -130,12 +76,11 @@ def run_cmdline(argv=None, account_plugins=None):
ac.set_config("mvbox_move", "0") ac.set_config("mvbox_move", "0")
ac.set_config("mvbox_watch", "0") ac.set_config("mvbox_watch", "0")
ac.set_config("sentbox_watch", "0") ac.set_config("sentbox_watch", "0")
ac.configure()
for plugin in account_plugins or []: ac.wait_configure_finish()
ac.add_account_plugin(plugin)
# start IO threads and configure if neccessary # start IO threads and configure if neccessary
ac.start() ac.start_io()
print("{}: waiting for message".format(ac.get_config("addr"))) print("{}: waiting for message".format(ac.get_config("addr")))

View File

@@ -45,22 +45,9 @@ def ffibuilder():
'deltachat.capi', 'deltachat.capi',
""" """
#include <deltachat.h> #include <deltachat.h>
const char * dupstring_helper(const char* string) int dc_event_has_string_data(int e)
{ {
return strdup(string); return DC_EVENT_DATA2_IS_STRING(e);
}
int dc_get_event_signature_types(int e)
{
int result = 0;
if (DC_EVENT_DATA1_IS_STRING(e))
result |= 1;
if (DC_EVENT_DATA2_IS_STRING(e))
result |= 2;
if (DC_EVENT_RETURNS_STRING(e))
result |= 4;
if (DC_EVENT_RETURNS_INT(e))
result |= 8;
return result;
} }
""", """,
include_dirs=incs, include_dirs=incs,
@@ -71,8 +58,7 @@ def ffibuilder():
builder.cdef(""" builder.cdef("""
typedef int... time_t; typedef int... time_t;
void free(void *ptr); void free(void *ptr);
extern const char * dupstring_helper(const char* string); extern int dc_event_has_string_data(int);
extern int dc_get_event_signature_types(int);
""") """)
distutils.log.set_verbosity(distutils.log.INFO) distutils.log.set_verbosity(distutils.log.INFO)
cc = distutils.ccompiler.new_compiler(force=True) cc = distutils.ccompiler.new_compiler(force=True)
@@ -92,13 +78,6 @@ def ffibuilder():
finally: finally:
shutil.rmtree(tmpdir) shutil.rmtree(tmpdir)
builder.cdef("""
extern "Python" uintptr_t py_dc_callback(
dc_context_t* context,
int event,
uintptr_t data1,
uintptr_t data2);
""")
return builder return builder

View File

@@ -1,22 +1,20 @@
""" Account class implementation. """ """ Account class implementation. """
from __future__ import print_function from __future__ import print_function
import atexit
from contextlib import contextmanager from contextlib import contextmanager
from email.utils import parseaddr from email.utils import parseaddr
import queue
from threading import Event from threading import Event
import os import os
from array import array from array import array
import deltachat
from . import const from . import const
from .capi import ffi, lib from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
from .chat import Chat from .chat import Chat
from .message import Message, map_system_message from .message import Message
from .contact import Contact from .contact import Contact
from .tracker import ImexTracker from .tracker import ImexTracker, ConfigureTracker
from . import hookspec, iothreads from . import hookspec
from .events import EventThread
class MissingCredentials(ValueError): class MissingCredentials(ValueError):
@@ -40,28 +38,24 @@ class Account(object):
# initialize per-account plugin system # initialize per-account plugin system
self._pm = hookspec.PerAccount._make_plugin_manager() self._pm = hookspec.PerAccount._make_plugin_manager()
self._logging = logging self._logging = logging
self.add_account_plugin(self) self.add_account_plugin(self)
self._dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
_destroy_dc_context,
)
hook = hookspec.Global._get_plugin_manager().hook
self._threads = iothreads.IOThreads(self)
self._hook_event_queue = queue.Queue()
self._in_use_iter_events = False
self._shutdown_event = Event()
# open database
self.db_path = db_path self.db_path = db_path
if hasattr(db_path, "encode"): if hasattr(db_path, "encode"):
db_path = db_path.encode("utf8") db_path = db_path.encode("utf8")
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
raise ValueError("Could not dc_open: {}".format(db_path)) self._dc_context = ffi.gc(
lib.dc_context_new(as_dc_charpointer(os_name), db_path, ffi.NULL),
lib.dc_context_unref,
)
if self._dc_context == ffi.NULL:
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
self._shutdown_event = Event()
self._event_thread = EventThread(self)
self._configkeys = self.get_config("sys.config_keys").split() self._configkeys = self.get_config("sys.config_keys").split()
atexit.register(self.shutdown) hook = hookspec.Global._get_plugin_manager().hook
hook.dc_account_init(account=self) hook.dc_account_init(account=self)
def disable_logging(self): def disable_logging(self):
@@ -72,16 +66,10 @@ class Account(object):
""" re-enable logging. """ """ re-enable logging. """
self._logging = True self._logging = True
@hookspec.account_hookimpl
def ac_process_ffi_event(self, ffi_event):
for name, kwargs in self._map_ffi_event(ffi_event):
ev = HookEvent(self, name=name, kwargs=kwargs)
self._hook_event_queue.put(ev)
# def __del__(self): # def __del__(self):
# self.shutdown() # self.shutdown()
def ac_log_line(self, msg): def log(self, msg):
if self._logging: if self._logging:
self._pm.hook.ac_log_line(message=msg) self._pm.hook.ac_log_line(message=msg)
@@ -172,7 +160,7 @@ class Account(object):
:returns: True if account is configured. :returns: True if account is configured.
""" """
return bool(lib.dc_is_configured(self._dc_context)) return True if lib.dc_is_configured(self._dc_context) else False
def set_avatar(self, img_path): def set_avatar(self, img_path):
"""Set self avatar. """Set self avatar.
@@ -250,7 +238,7 @@ class Account(object):
:returns: True if deletion succeeded (contact was deleted) :returns: True if deletion succeeded (contact was deleted)
""" """
contact_id = contact.id contact_id = contact.id
assert contact._dc_context == self._dc_context assert contact.account == self
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
return bool(lib.dc_delete_contact(self._dc_context, contact_id)) return bool(lib.dc_delete_contact(self._dc_context, contact_id))
@@ -298,7 +286,7 @@ class Account(object):
:returns: a :class:`deltachat.chat.Chat` object. :returns: a :class:`deltachat.chat.Chat` object.
""" """
if hasattr(contact, "id"): if hasattr(contact, "id"):
if contact._dc_context != self._dc_context: if contact.account != self:
raise ValueError("Contact belongs to a different Account") raise ValueError("Contact belongs to a different Account")
contact_id = contact.id contact_id = contact.id
else: else:
@@ -318,7 +306,7 @@ class Account(object):
:returns: a :class:`deltachat.chat.Chat` object. :returns: a :class:`deltachat.chat.Chat` object.
""" """
if hasattr(message, "id"): if hasattr(message, "id"):
if self._dc_context != message._dc_context: if message.account != self:
raise ValueError("Message belongs to a different Account") raise ValueError("Message belongs to a different Account")
msg_id = message.id msg_id = message.id
else: else:
@@ -438,8 +426,6 @@ class Account(object):
def _export(self, path, imex_cmd): def _export(self, path, imex_cmd):
with self.temp_plugin(ImexTracker()) as imex_tracker: with self.temp_plugin(ImexTracker()) as imex_tracker:
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL) lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
return imex_tracker.wait_finish() return imex_tracker.wait_finish()
def import_self_keys(self, path): def import_self_keys(self, path):
@@ -462,8 +448,6 @@ class Account(object):
def _import(self, path, imex_cmd): def _import(self, path, imex_cmd):
with self.temp_plugin(ImexTracker()) as imex_tracker: with self.temp_plugin(ImexTracker()) as imex_tracker:
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL) lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
imex_tracker.wait_finish() imex_tracker.wait_finish()
def initiate_key_transfer(self): def initiate_key_transfer(self):
@@ -472,8 +456,8 @@ class Account(object):
If sending out was unsuccessful, a RuntimeError is raised. If sending out was unsuccessful, a RuntimeError is raised.
""" """
self.check_is_configured() self.check_is_configured()
if not self._threads.is_started(): if not self.is_started():
raise RuntimeError("threads not running, can not send out") raise RuntimeError("IO not running, can not send out")
res = lib.dc_initiate_key_transfer(self._dc_context) res = lib.dc_initiate_key_transfer(self._dc_context)
if res == ffi.NULL: if res == ffi.NULL:
raise RuntimeError("could not send out autocrypt setup message") raise RuntimeError("could not send out autocrypt setup message")
@@ -555,6 +539,10 @@ class Account(object):
self._pm.check_pending() self._pm.check_pending()
return plugin return plugin
def remove_account_plugin(self, plugin, name=None):
""" remove an account plugin. """
self._pm.unregister(plugin, name=name)
@contextmanager @contextmanager
def temp_plugin(self, plugin): def temp_plugin(self, plugin):
""" run a with-block with the given plugin temporarily registered. """ """ run a with-block with the given plugin temporarily registered. """
@@ -566,110 +554,82 @@ class Account(object):
""" Stop ongoing securejoin, configuration or other core jobs. """ """ Stop ongoing securejoin, configuration or other core jobs. """
lib.dc_stop_ongoing_process(self._dc_context) lib.dc_stop_ongoing_process(self._dc_context)
def start(self, callback_thread=True): def start_io(self):
""" start this account (activate imap/smtp threads etc.) """ start this account's IO scheduling (Rust-core async scheduler)
and return immediately.
If this account is not configured, an internal configuration If this account is not configured an Exception is raised.
job will be scheduled if config values are sufficiently specified. You need to call account.configure() and account.wait_configure_finish()
before.
You may call `wait_shutdown` or `shutdown` after the You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the
account is in started mode. account is started.
:raises MissingCredentials: if `addr` and `mail_pw` values are not set. :raises MissingCredentials: if `addr` and `mail_pw` values are not set.
:raises ConfigureFailed: if the account could not be configured.
:returns: None :returns: None (account is configured and with io-scheduling running)
""" """
if not self.is_configured(): if not self.is_configured():
if not self.get_config("addr") or not self.get_config("mail_pw"): raise ValueError("account not configured, cannot start io")
raise MissingCredentials("addr or mail_pwd not set in config") lib.dc_start_io(self._dc_context)
lib.dc_configure(self._dc_context)
self._threads.start(callback_thread=callback_thread) def configure(self):
assert not self.is_configured()
assert not hasattr(self, "_configtracker")
if not self.get_config("addr") or not self.get_config("mail_pw"):
raise MissingCredentials("addr or mail_pwd not set in config")
if hasattr(self, "_configtracker"):
self.remove_account_plugin(self._configtracker)
self._configtracker = ConfigureTracker()
self.add_account_plugin(self._configtracker)
lib.dc_configure(self._dc_context)
def wait_configure_finish(self):
try:
self._configtracker.wait_finish()
finally:
self.remove_account_plugin(self._configtracker)
del self._configtracker
def is_started(self):
return self._event_thread.is_alive() and bool(lib.dc_is_io_running(self._dc_context))
def wait_shutdown(self): def wait_shutdown(self):
""" wait until shutdown of this account has completed. """ """ wait until shutdown of this account has completed. """
self._shutdown_event.wait() self._shutdown_event.wait()
def shutdown(self, wait=True): def stop_io(self):
""" shutdown account, stop threads and close and remove """ stop core IO scheduler if it is running. """
underlying dc_context and callbacks. """ self.log("stop_ongoing")
dc_context = self._dc_context self.stop_ongoing()
if dc_context is None:
if bool(lib.dc_is_io_running(self._dc_context)):
self.log("dc_stop_io (stop core IO scheduler)")
lib.dc_stop_io(self._dc_context)
else:
self.log("stop_scheduler called on non-running context")
def shutdown(self):
""" shutdown and destroy account (stop callback thread, close and remove
underlying dc_context)."""
if self._dc_context is None:
return return
if self._threads.is_started(): self.stop_io()
self.stop_ongoing()
self._threads.stop(wait=False) self.log("remove dc_context references")
lib.dc_close(dc_context) # the dc_context_unref triggers get_next_event to return ffi.NULL
self._hook_event_queue.put(None) # which in turns makes the event thread finish execution
self._threads.stop(wait=wait) # to wait for threads
self._dc_context = None self._dc_context = None
atexit.unregister(self.shutdown)
self.log("wait for event thread to finish")
self._event_thread.wait()
self._shutdown_event.set() self._shutdown_event.set()
hook = hookspec.Global._get_plugin_manager().hook hook = hookspec.Global._get_plugin_manager().hook
hook.dc_account_after_shutdown(account=self, dc_context=dc_context) hook.dc_account_after_shutdown(account=self)
self.log("shutdown finished")
def _handle_current_events(self):
""" handle all currently queued events and then return. """
while 1:
try:
event = self._hook_event_queue.get(block=False)
except queue.Empty:
break
else:
event.call_hook()
def iter_events(self, timeout=None):
""" yield hook events until shutdown.
It is not allowed to call iter_events() from multiple threads.
"""
if self._in_use_iter_events:
raise RuntimeError("can only call iter_events() from one thread")
self._in_use_iter_events = True
while 1:
event = self._hook_event_queue.get(timeout=timeout)
if event is None:
break
yield event
def _map_ffi_event(self, ffi_event):
name = ffi_event.name
if name == "DC_EVENT_CONFIGURE_PROGRESS":
data1 = ffi_event.data1
if data1 == 0 or data1 == 1000:
success = data1 == 1000
yield "ac_configure_completed", dict(success=success)
elif name == "DC_EVENT_INCOMING_MSG":
msg = self.get_message_by_id(ffi_event.data2)
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
elif name == "DC_EVENT_MSGS_CHANGED":
if ffi_event.data2 != 0:
msg = self.get_message_by_id(ffi_event.data2)
if msg.is_outgoing():
res = map_system_message(msg)
if res and res[0].startswith("ac_member"):
yield res
yield "ac_outgoing_message", dict(message=msg)
elif msg.is_in_fresh():
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
elif name == "DC_EVENT_MSG_DELIVERED":
msg = self.get_message_by_id(ffi_event.data2)
yield "ac_message_delivered", dict(message=msg)
elif name == "DC_EVENT_CHAT_MODIFIED":
chat = self.get_chat_by_id(ffi_event.data1)
yield "ac_chat_modified", dict(chat=chat)
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
# destructor for dc_context
dc_context_unref(dc_context)
try:
deltachat.clear_context_callback(dc_context)
except (TypeError, AttributeError):
# we are deep into Python Interpreter shutdown,
# so no need to clear the callback context mapping.
pass
class ScannedQRCode: class ScannedQRCode:
@@ -685,17 +645,3 @@ class ScannedQRCode:
@property @property
def contact_id(self): def contact_id(self):
return self._dc_lot.id() return self._dc_lot.id()
class HookEvent:
def __init__(self, account, name, kwargs):
assert hasattr(account._pm.hook, name), name
self.account = account
self.name = name
self.kwargs = kwargs
def call_hook(self):
hook = getattr(self.account._pm.hook, self.name, None)
if hook is None:
raise ValueError("event_name {} unknown".format(self.name))
return hook(**self.kwargs)

View File

@@ -19,12 +19,11 @@ class Chat(object):
def __init__(self, account, id): def __init__(self, account, id):
self.account = account self.account = account
self._dc_context = account._dc_context
self.id = id self.id = id
def __eq__(self, other): def __eq__(self, other):
return self.id == getattr(other, "id", None) and \ return self.id == getattr(other, "id", None) and \
self._dc_context == getattr(other, "_dc_context", None) self.account._dc_context == other.account._dc_context
def __ne__(self, other): def __ne__(self, other):
return not (self == other) return not (self == other)
@@ -35,7 +34,7 @@ class Chat(object):
@property @property
def _dc_chat(self): def _dc_chat(self):
return ffi.gc( return ffi.gc(
lib.dc_get_chat(self._dc_context, self.id), lib.dc_get_chat(self.account._dc_context, self.id),
lib.dc_chat_unref lib.dc_chat_unref
) )
@@ -47,7 +46,7 @@ class Chat(object):
- does not delete messages on server - does not delete messages on server
- the chat or contact is not blocked, new message will arrive - the chat or contact is not blocked, new message will arrive
""" """
lib.dc_delete_chat(self._dc_context, self.id) lib.dc_delete_chat(self.account._dc_context, self.id)
# ------ chat status/metadata API ------------------------------ # ------ chat status/metadata API ------------------------------
@@ -105,7 +104,7 @@ class Chat(object):
:returns: None :returns: None
""" """
name = as_dc_charpointer(name) name = as_dc_charpointer(name)
return lib.dc_set_chat_name(self._dc_context, self.id, name) return lib.dc_set_chat_name(self.account._dc_context, self.id, name)
def mute(self, duration=None): def mute(self, duration=None):
""" mutes the chat """ mutes the chat
@@ -117,7 +116,7 @@ class Chat(object):
mute_duration = -1 mute_duration = -1
else: else:
mute_duration = duration mute_duration = duration
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration) ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration)
if not bool(ret): if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed") raise ValueError("Call to dc_set_chat_mute_duration failed")
@@ -126,7 +125,7 @@ class Chat(object):
:returns: None :returns: None
""" """
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0) ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, 0)
if not bool(ret): if not bool(ret):
raise ValueError("Failed to unmute chat") raise ValueError("Failed to unmute chat")
@@ -152,7 +151,7 @@ class Chat(object):
in a second channel (typically used by mobiles with QRcode-show + scan UX) in a second channel (typically used by mobiles with QRcode-show + scan UX)
where account.join_with_qrcode(qr) needs to be called. where account.join_with_qrcode(qr) needs to be called.
""" """
res = lib.dc_get_securejoin_qr(self._dc_context, self.id) res = lib.dc_get_securejoin_qr(self.account._dc_context, self.id)
return from_dc_charpointer(res) return from_dc_charpointer(res)
# ------ chat messaging API ------------------------------ # ------ chat messaging API ------------------------------
@@ -174,7 +173,7 @@ class Chat(object):
assert msg.id != 0 assert msg.id != 0
# get a fresh copy of dc_msg, the core needs it # get a fresh copy of dc_msg, the core needs it
msg = Message.from_db(self.account, msg.id) msg = Message.from_db(self.account, msg.id)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg) sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
if sent_id == 0: if sent_id == 0:
raise ValueError("message could not be sent") raise ValueError("message could not be sent")
# modify message in place to avoid bad state for the caller # modify message in place to avoid bad state for the caller
@@ -189,7 +188,7 @@ class Chat(object):
:returns: the resulting :class:`deltachat.message.Message` instance :returns: the resulting :class:`deltachat.message.Message` instance
""" """
msg = as_dc_charpointer(text) msg = as_dc_charpointer(text)
msg_id = lib.dc_send_text_msg(self._dc_context, self.id, msg) msg_id = lib.dc_send_text_msg(self.account._dc_context, self.id, msg)
if msg_id == 0: if msg_id == 0:
raise ValueError("message could not be send, does chat exist?") raise ValueError("message could not be send, does chat exist?")
return Message.from_db(self.account, msg_id) return Message.from_db(self.account, msg_id)
@@ -204,7 +203,7 @@ class Chat(object):
""" """
msg = Message.new_empty(self.account, view_type="file") msg = Message.new_empty(self.account, view_type="file")
msg.set_file(path, mime_type) msg.set_file(path, mime_type)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg) sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
if sent_id == 0: if sent_id == 0:
raise ValueError("message could not be sent") raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id) return Message.from_db(self.account, sent_id)
@@ -219,7 +218,7 @@ class Chat(object):
mime_type = mimetypes.guess_type(path)[0] mime_type = mimetypes.guess_type(path)[0]
msg = Message.new_empty(self.account, view_type="image") msg = Message.new_empty(self.account, view_type="image")
msg.set_file(path, mime_type) msg.set_file(path, mime_type)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg) sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
if sent_id == 0: if sent_id == 0:
raise ValueError("message could not be sent") raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id) return Message.from_db(self.account, sent_id)
@@ -230,7 +229,7 @@ class Chat(object):
:param msg: the message to be prepared. :param msg: the message to be prepared.
:returns: :class:`deltachat.message.Message` instance. :returns: :class:`deltachat.message.Message` instance.
""" """
msg_id = lib.dc_prepare_msg(self._dc_context, self.id, msg._dc_msg) msg_id = lib.dc_prepare_msg(self.account._dc_context, self.id, msg._dc_msg)
if msg_id == 0: if msg_id == 0:
raise ValueError("message could not be prepared") raise ValueError("message could not be prepared")
# invalidate passed in message which is not safe to use anymore # invalidate passed in message which is not safe to use anymore
@@ -266,7 +265,7 @@ class Chat(object):
msg = Message.from_db(self.account, message.id) msg = Message.from_db(self.account, message.id)
# pass 0 as chat-id because core-docs say it's ok when out-preparing # pass 0 as chat-id because core-docs say it's ok when out-preparing
sent_id = lib.dc_send_msg(self._dc_context, 0, msg._dc_msg) sent_id = lib.dc_send_msg(self.account._dc_context, 0, msg._dc_msg)
if sent_id == 0: if sent_id == 0:
raise ValueError("message could not be sent") raise ValueError("message could not be sent")
assert sent_id == msg.id assert sent_id == msg.id
@@ -280,9 +279,9 @@ class Chat(object):
:returns: None :returns: None
""" """
if message is None: if message is None:
lib.dc_set_draft(self._dc_context, self.id, ffi.NULL) lib.dc_set_draft(self.account._dc_context, self.id, ffi.NULL)
else: else:
lib.dc_set_draft(self._dc_context, self.id, message._dc_msg) lib.dc_set_draft(self.account._dc_context, self.id, message._dc_msg)
def get_draft(self): def get_draft(self):
""" get draft message for this chat. """ get draft message for this chat.
@@ -290,7 +289,7 @@ class Chat(object):
:param message: a :class:`Message` instance :param message: a :class:`Message` instance
:returns: Message object or None (if no draft available) :returns: Message object or None (if no draft available)
""" """
x = lib.dc_get_draft(self._dc_context, self.id) x = lib.dc_get_draft(self.account._dc_context, self.id)
if x == ffi.NULL: if x == ffi.NULL:
return None return None
dc_msg = ffi.gc(x, lib.dc_msg_unref) dc_msg = ffi.gc(x, lib.dc_msg_unref)
@@ -302,7 +301,7 @@ class Chat(object):
:returns: list of :class:`deltachat.message.Message` objects for this chat. :returns: list of :class:`deltachat.message.Message` objects for this chat.
""" """
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_chat_msgs(self._dc_context, self.id, 0, 0), lib.dc_get_chat_msgs(self.account._dc_context, self.id, 0, 0),
lib.dc_array_unref lib.dc_array_unref
) )
return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x))) return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
@@ -312,18 +311,18 @@ class Chat(object):
:returns: number of fresh messages :returns: number of fresh messages
""" """
return lib.dc_get_fresh_msg_cnt(self._dc_context, self.id) return lib.dc_get_fresh_msg_cnt(self.account._dc_context, self.id)
def mark_noticed(self): def mark_noticed(self):
""" mark all messages in this chat as noticed. """ mark all messages in this chat as noticed.
Noticed messages are no longer fresh. Noticed messages are no longer fresh.
""" """
return lib.dc_marknoticed_chat(self._dc_context, self.id) return lib.dc_marknoticed_chat(self.account._dc_context, self.id)
def get_summary(self): def get_summary(self):
""" return dictionary with summary information. """ """ return dictionary with summary information. """
dc_res = lib.dc_chat_get_info_json(self._dc_context, self.id) dc_res = lib.dc_chat_get_info_json(self.account._dc_context, self.id)
s = from_dc_charpointer(dc_res) s = from_dc_charpointer(dc_res)
return json.loads(s) return json.loads(s)
@@ -336,7 +335,7 @@ class Chat(object):
:raises ValueError: if contact could not be added :raises ValueError: if contact could not be added
:returns: None :returns: None
""" """
ret = lib.dc_add_contact_to_chat(self._dc_context, self.id, contact.id) ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
if ret != 1: if ret != 1:
raise ValueError("could not add contact {!r} to chat".format(contact)) raise ValueError("could not add contact {!r} to chat".format(contact))
@@ -347,7 +346,7 @@ class Chat(object):
:raises ValueError: if contact could not be removed :raises ValueError: if contact could not be removed
:returns: None :returns: None
""" """
ret = lib.dc_remove_contact_from_chat(self._dc_context, self.id, contact.id) ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id)
if ret != 1: if ret != 1:
raise ValueError("could not remove contact {!r} from chat".format(contact)) raise ValueError("could not remove contact {!r} from chat".format(contact))
@@ -359,7 +358,7 @@ class Chat(object):
""" """
from .contact import Contact from .contact import Contact
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_chat_contacts(self._dc_context, self.id), lib.dc_get_chat_contacts(self.account._dc_context, self.id),
lib.dc_array_unref lib.dc_array_unref
) )
return list(iter_array( return list(iter_array(
@@ -378,7 +377,7 @@ class Chat(object):
""" """
assert os.path.exists(img_path), img_path assert os.path.exists(img_path), img_path
p = as_dc_charpointer(img_path) p = as_dc_charpointer(img_path)
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, p) res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, p)
if res != 1: if res != 1:
raise ValueError("Setting Profile Image {!r} failed".format(p)) raise ValueError("Setting Profile Image {!r} failed".format(p))
@@ -391,7 +390,7 @@ class Chat(object):
:raises ValueError: if profile image could not be reset :raises ValueError: if profile image could not be reset
:returns: None :returns: None
""" """
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, ffi.NULL) res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, ffi.NULL)
if res != 1: if res != 1:
raise ValueError("Removing Profile Image failed") raise ValueError("Removing Profile Image failed")
@@ -421,7 +420,7 @@ class Chat(object):
"""return True if this chat has location-sending enabled currently. """return True if this chat has location-sending enabled currently.
:returns: True if location sending is enabled. :returns: True if location sending is enabled.
""" """
return lib.dc_is_sending_locations_to_chat(self._dc_context, self.id) return lib.dc_is_sending_locations_to_chat(self.account._dc_context, self.id)
def is_archived(self): def is_archived(self):
"""return True if this chat is archived. """return True if this chat is archived.
@@ -434,7 +433,7 @@ class Chat(object):
all subsequent messages will carry a location with them. all subsequent messages will carry a location with them.
""" """
lib.dc_send_locations_to_chat(self._dc_context, self.id, seconds) lib.dc_send_locations_to_chat(self.account._dc_context, self.id, seconds)
def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None): def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None):
"""return list of locations for the given contact in the given timespan. """return list of locations for the given contact in the given timespan.
@@ -458,7 +457,7 @@ class Chat(object):
else: else:
contact_id = contact.id contact_id = contact.id
dc_array = lib.dc_get_locations(self._dc_context, self.id, contact_id, time_from, time_to) dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to)
return [ return [
Location( Location(
latitude=lib.dc_array_get_latitude(dc_array, i), latitude=lib.dc_array_get_latitude(dc_array, i),

View File

@@ -12,22 +12,21 @@ class Contact(object):
""" """
def __init__(self, account, id): def __init__(self, account, id):
self.account = account self.account = account
self._dc_context = account._dc_context
self.id = id self.id = id
def __eq__(self, other): def __eq__(self, other):
return self._dc_context == other._dc_context and self.id == other.id return self.account._dc_context == other.account._dc_context and self.id == other.id
def __ne__(self, other): def __ne__(self, other):
return not (self == other) return not (self == other)
def __repr__(self): def __repr__(self):
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context) return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context)
@property @property
def _dc_contact(self): def _dc_contact(self):
return ffi.gc( return ffi.gc(
lib.dc_get_contact(self._dc_context, self.id), lib.dc_get_contact(self.account._dc_context, self.id),
lib.dc_contact_unref lib.dc_contact_unref
) )

View File

@@ -1,137 +0,0 @@
import deltachat
import threading
import time
import re
from queue import Queue, Empty
from .hookspec import account_hookimpl, global_hookimpl
@global_hookimpl
def dc_account_init(account):
# send all FFI events for this account to a plugin hook
def _ll_event(ctx, evt_name, data1, data2):
assert ctx == account._dc_context
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
account._pm.hook.ac_process_ffi_event(
account=account, ffi_event=ffi_event
)
deltachat.set_context_callback(account._dc_context, _ll_event)
@global_hookimpl
def dc_account_after_shutdown(dc_context):
deltachat.clear_context_callback(dc_context)
class FFIEvent:
def __init__(self, name, data1, data2):
self.name = name
self.data1 = data1
self.data2 = data2
def __str__(self):
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
class FFIEventLogger:
""" If you register an instance of this logger with an Account
you'll get all ffi-events printed.
"""
# to prevent garbled logging
_loglock = threading.RLock()
def __init__(self, account, logid):
"""
:param logid: an optional logging prefix that should be used with
the default internal logging.
"""
self.account = account
self.logid = logid
self.init_time = time.time()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self._log_event(ffi_event)
def _log_event(self, ffi_event):
# don't show events that are anyway empty impls now
if ffi_event.name == "DC_EVENT_GET_STRING":
return
self.account.ac_log_line(str(ffi_event))
@account_hookimpl
def ac_log_line(self, message):
t = threading.currentThread()
tname = getattr(t, "name", t)
if tname == "MainThread":
tname = "MAIN"
elapsed = time.time() - self.init_time
locname = tname
if self.logid:
locname += "-" + self.logid
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
with self._loglock:
print(s, flush=True)
class FFIEventTracker:
def __init__(self, account, timeout=None):
self.account = account
self._timeout = timeout
self._event_queue = Queue()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self._event_queue.put(ffi_event)
def set_timeout(self, timeout):
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 if timeout is not None else self._timeout
ev = self._event_queue.get(timeout=timeout)
if check_error and ev.name == "DC_EVENT_ERROR":
raise ValueError(str(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.name), "event found {}".format(ev)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
self.account.ac_log_line("-- 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.name):
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.data2):
return ev
def wait_next_incoming_message(self):
""" wait for and return next incoming message. """
ev = self.get_matching("DC_EVENT_INCOMING_MSG")
return self.account.get_message_by_id(ev.data2)
def wait_next_messages_changed(self):
""" wait for and return next message-changed message or None
if the event contains no msgid"""
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data2 > 0:
return self.account.get_message_by_id(ev.data2)

View File

@@ -0,0 +1,217 @@
import threading
import time
import re
from queue import Queue, Empty
import deltachat
from .hookspec import account_hookimpl
from contextlib import contextmanager
from .capi import ffi, lib
from .message import map_system_message
from .cutil import from_dc_charpointer
class FFIEvent:
def __init__(self, name, data1, data2):
self.name = name
self.data1 = data1
self.data2 = data2
def __str__(self):
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
class FFIEventLogger:
""" If you register an instance of this logger with an Account
you'll get all ffi-events printed.
"""
# to prevent garbled logging
_loglock = threading.RLock()
def __init__(self, account, logid):
"""
:param logid: an optional logging prefix that should be used with
the default internal logging.
"""
self.account = account
self.logid = logid
self.init_time = time.time()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self.account.log(str(ffi_event))
@account_hookimpl
def ac_log_line(self, message):
t = threading.currentThread()
tname = getattr(t, "name", t)
if tname == "MainThread":
tname = "MAIN"
elapsed = time.time() - self.init_time
locname = tname
if self.logid:
locname += "-" + self.logid
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
with self._loglock:
print(s, flush=True)
class FFIEventTracker:
def __init__(self, account, timeout=None):
self.account = account
self._timeout = timeout
self._event_queue = Queue()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self._event_queue.put(ffi_event)
def set_timeout(self, timeout):
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 if timeout is not None else self._timeout
ev = self._event_queue.get(timeout=timeout)
if check_error and ev.name == "DC_EVENT_ERROR":
raise ValueError("unexpected event: {}".format(ev))
return ev
def iter_events(self, timeout=None, check_error=True):
while 1:
yield self.get(timeout=timeout, check_error=check_error)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
rex = re.compile("(?:{}).*".format(event_name_regex))
for ev in self.iter_events(timeout=timeout, check_error=check_error):
if rex.match(ev.name):
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.data2):
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.name), "event found {}".format(ev)
def wait_securejoin_inviter_progress(self, target):
while 1:
event = self.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
if event.data2 >= target:
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
break
def wait_next_incoming_message(self):
""" wait for and return next incoming message. """
ev = self.get_matching("DC_EVENT_INCOMING_MSG")
return self.account.get_message_by_id(ev.data2)
def wait_next_messages_changed(self):
""" wait for and return next message-changed message or None
if the event contains no msgid"""
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data2 > 0:
return self.account.get_message_by_id(ev.data2)
class EventThread(threading.Thread):
""" Event Thread for an account.
With each Account init this callback thread is started.
"""
def __init__(self, account):
self.account = account
super(EventThread, self).__init__(name="events")
self.setDaemon(True)
self.start()
@contextmanager
def log_execution(self, message):
self.account.log(message + " START")
yield
self.account.log(message + " FINISHED")
def wait(self):
if self == threading.current_thread():
# we are in the callback thread and thus cannot
# wait for the thread-loop to finish.
return
self.join()
def run(self):
""" get and run events until shutdown. """
with self.log_execution("EVENT THREAD"):
self._inner_run()
def _inner_run(self):
event_emitter = ffi.gc(
lib.dc_get_event_emitter(self.account._dc_context),
lib.dc_event_emitter_unref,
)
while 1:
event = lib.dc_get_next_event(event_emitter)
if event == ffi.NULL:
break
evt = lib.dc_event_get_id(event)
data1 = lib.dc_event_get_data1_int(event)
# the following code relates to the deltachat/_build.py's helper
# function which provides us signature info of an event call
evt_name = deltachat.get_dc_event_name(evt)
if lib.dc_event_has_string_data(evt):
data2 = from_dc_charpointer(lib.dc_event_get_data2_str(event))
else:
data2 = lib.dc_event_get_data2_int(event)
lib.dc_event_unref(event)
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
try:
self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event)
for name, kwargs in self._map_ffi_event(ffi_event):
self.account.log("calling hook name={} kwargs={}".format(name, kwargs))
hook = getattr(self.account._pm.hook, name)
hook(**kwargs)
except Exception:
if self.account._dc_context is not None:
raise
def _map_ffi_event(self, ffi_event):
name = ffi_event.name
account = self.account
if name == "DC_EVENT_CONFIGURE_PROGRESS":
data1 = ffi_event.data1
if data1 == 0 or data1 == 1000:
success = data1 == 1000
yield "ac_configure_completed", dict(success=success)
elif name == "DC_EVENT_INCOMING_MSG":
msg = account.get_message_by_id(ffi_event.data2)
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
elif name == "DC_EVENT_MSGS_CHANGED":
if ffi_event.data2 != 0:
msg = account.get_message_by_id(ffi_event.data2)
if msg.is_outgoing():
res = map_system_message(msg)
if res and res[0].startswith("ac_member"):
yield res
yield "ac_outgoing_message", dict(message=msg)
elif msg.is_in_fresh():
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
elif name == "DC_EVENT_MSG_DELIVERED":
msg = account.get_message_by_id(ffi_event.data2)
yield "ac_message_delivered", dict(message=msg)
elif name == "DC_EVENT_CHAT_MODIFIED":
chat = account.get_chat_by_id(ffi_event.data1)
yield "ac_chat_modified", dict(chat=chat)

View File

@@ -15,8 +15,9 @@ global_hookimpl = pluggy.HookimplMarker(global_spec_name)
class PerAccount: class PerAccount:
""" per-Account-instance hook specifications. """ per-Account-instance hook specifications.
Except for ac_process_ffi_event all hooks are executed All hooks are executed in a dedicated Event thread.
in the thread which calls Account.wait_shutdown(). Hooks are not allowed to block/last long as this
blocks overall event processing on the python side.
""" """
@classmethod @classmethod
def _make_plugin_manager(cls): def _make_plugin_manager(cls):
@@ -88,5 +89,5 @@ class Global:
""" called when `Account::__init__()` function starts executing. """ """ called when `Account::__init__()` function starts executing. """
@global_hookspec @global_hookspec
def dc_account_after_shutdown(self, account, dc_context): def dc_account_after_shutdown(self, account):
""" Called after the account has been shutdown. """ """ Called after the account has been shutdown. """

View File

@@ -1,106 +0,0 @@
import threading
import time
from contextlib import contextmanager
from .capi import lib
class IOThreads:
def __init__(self, account):
self.account = account
self._dc_context = account._dc_context
self._thread_quitflag = False
self._name2thread = {}
def is_started(self):
return len(self._name2thread) > 0
def start(self, callback_thread):
assert not self.is_started()
self._start_one_thread("inbox", self.imap_thread_run)
self._start_one_thread("smtp", self.smtp_thread_run)
if callback_thread:
self._start_one_thread("cb", self.cb_thread_run)
if int(self.account.get_config("mvbox_watch")):
self._start_one_thread("mvbox", self.mvbox_thread_run)
if int(self.account.get_config("sentbox_watch")):
self._start_one_thread("sentbox", self.sentbox_thread_run)
def _start_one_thread(self, name, func):
self._name2thread[name] = t = threading.Thread(target=func, name=name)
t.setDaemon(1)
t.start()
@contextmanager
def log_execution(self, message):
self.account.ac_log_line(message + " START")
yield
self.account.ac_log_line(message + " FINISHED")
def stop(self, wait=False):
self._thread_quitflag = True
# Workaround for a race condition. Make sure that thread is
# not in between checking for quitflag and entering idle.
time.sleep(0.5)
lib.dc_interrupt_imap_idle(self._dc_context)
lib.dc_interrupt_smtp_idle(self._dc_context)
if "mvbox" in self._name2thread:
lib.dc_interrupt_mvbox_idle(self._dc_context)
if "sentbox" in self._name2thread:
lib.dc_interrupt_sentbox_idle(self._dc_context)
if wait:
for name, thread in self._name2thread.items():
if thread != threading.currentThread():
thread.join()
def cb_thread_run(self):
with self.log_execution("CALLBACK THREAD START"):
it = self.account.iter_events()
while not self._thread_quitflag:
try:
ev = next(it)
except StopIteration:
break
self.account.ac_log_line("calling hook name={} kwargs={}".format(ev.name, ev.kwargs))
ev.call_hook()
def imap_thread_run(self):
with self.log_execution("INBOX THREAD START"):
while not self._thread_quitflag:
lib.dc_perform_imap_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_idle(self._dc_context)
def mvbox_thread_run(self):
with self.log_execution("MVBOX THREAD"):
while not self._thread_quitflag:
lib.dc_perform_mvbox_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_idle(self._dc_context)
def sentbox_thread_run(self):
with self.log_execution("SENTBOX THREAD"):
while not self._thread_quitflag:
lib.dc_perform_sentbox_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_idle(self._dc_context)
def smtp_thread_run(self):
with self.log_execution("SMTP THREAD"):
while not self._thread_quitflag:
lib.dc_perform_smtp_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_smtp_idle(self._dc_context)

View File

@@ -16,8 +16,7 @@ class Message(object):
""" """
def __init__(self, account, dc_msg): def __init__(self, account, dc_msg):
self.account = account self.account = account
self._dc_context = account._dc_context assert isinstance(self.account._dc_context, ffi.CData)
assert isinstance(self._dc_context, ffi.CData)
assert isinstance(dc_msg, ffi.CData) assert isinstance(dc_msg, ffi.CData)
assert dc_msg != ffi.NULL assert dc_msg != ffi.NULL
self._dc_msg = dc_msg self._dc_msg = dc_msg
@@ -29,8 +28,10 @@ class Message(object):
def __repr__(self): def __repr__(self):
c = self.get_sender_contact() c = self.get_sender_contact()
return "<Message id={} sender={}/{} outgoing={} chat={}/{}>".format( typ = "outgoing" if self.is_outgoing() else "incoming"
self.id, c.id, c.addr, self.is_outgoing(), self.chat.id, self.chat.get_name()) return "<Message {} sys={} {} id={} sender={}/{} chat={}/{}>".format(
typ, self.is_system_message(), repr(self.text[:10]),
self.id, c.id, c.addr, self.chat.id, self.chat.get_name())
@classmethod @classmethod
def from_db(cls, account, id): def from_db(cls, account, id):
@@ -58,7 +59,7 @@ class Message(object):
""" """
self.account.create_chat_by_message(self) self.account.create_chat_by_message(self)
self._dc_msg = ffi.gc( self._dc_msg = ffi.gc(
lib.dc_get_msg(self._dc_context, self.id), lib.dc_get_msg(self.account._dc_context, self.id),
lib.dc_msg_unref lib.dc_msg_unref
) )
@@ -95,7 +96,7 @@ class Message(object):
def is_system_message(self): def is_system_message(self):
""" return True if this message is a system/info message. """ """ return True if this message is a system/info message. """
return lib.dc_msg_is_info(self._dc_msg) return bool(lib.dc_msg_is_info(self._dc_msg))
def is_setup_message(self): def is_setup_message(self):
""" return True if this message is a setup message. """ """ return True if this message is a setup message. """
@@ -118,12 +119,12 @@ class Message(object):
The text is multiline and may contain eg. the raw text of the message. The text is multiline and may contain eg. the raw text of the message.
""" """
return from_dc_charpointer(lib.dc_get_msg_info(self._dc_context, self.id)) return from_dc_charpointer(lib.dc_get_msg_info(self.account._dc_context, self.id))
def continue_key_transfer(self, setup_code): def continue_key_transfer(self, setup_code):
""" extract key and use it as primary key for this account. """ """ extract key and use it as primary key for this account. """
res = lib.dc_continue_key_transfer( res = lib.dc_continue_key_transfer(
self._dc_context, self.account._dc_context,
self.id, self.id,
as_dc_charpointer(setup_code) as_dc_charpointer(setup_code)
) )
@@ -158,7 +159,7 @@ class Message(object):
:returns: email-mime message object (with headers only, no body). :returns: email-mime message object (with headers only, no body).
""" """
import email.parser import email.parser
mime_headers = lib.dc_get_mime_headers(self._dc_context, self.id) mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
if mime_headers: if mime_headers:
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref)) s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
if isinstance(s, bytes): if isinstance(s, bytes):
@@ -201,7 +202,7 @@ class Message(object):
else: else:
# load message from db to get a fresh/current state # load message from db to get a fresh/current state
dc_msg = ffi.gc( dc_msg = ffi.gc(
lib.dc_get_msg(self._dc_context, self.id), lib.dc_get_msg(self.account._dc_context, self.id),
lib.dc_msg_unref lib.dc_msg_unref
) )
return lib.dc_msg_get_state(dc_msg) return lib.dc_msg_get_state(dc_msg)

View File

@@ -13,10 +13,8 @@ import pytest
import requests import requests
from . import Account, const from . import Account, const
from .tracker import ConfigureTracker
from .capi import lib from .capi import lib
from .eventlogger import FFIEventLogger, FFIEventTracker from .events import FFIEventLogger, FFIEventTracker
from _pytest.monkeypatch import MonkeyPatch
from _pytest._code import Source from _pytest._code import Source
import deltachat import deltachat
@@ -74,6 +72,9 @@ def pytest_configure(config):
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item): def pytest_runtest_setup(self, item):
if item.get_closest_marker("ignored"):
if not item.config.getvalue("ignored"):
pytest.skip("use --ignored to run this test")
self.enable_logging(item) self.enable_logging(item)
yield yield
self.disable_logging(item) self.disable_logging(item)
@@ -99,18 +100,16 @@ def pytest_report_header(config, startdir):
summary = [] summary = []
t = tempfile.mktemp() t = tempfile.mktemp()
m = MonkeyPatch()
try: try:
m.setattr(sys.stdout, "write", lambda x: len(x))
ac = Account(t) ac = Account(t)
info = ac.get_info() info = ac.get_info()
ac.shutdown() ac.shutdown()
finally: finally:
m.undo()
os.remove(t) os.remove(t)
summary.extend(['Deltachat core={} sqlite={}'.format( summary.extend(['Deltachat core={} sqlite={} journal_mode={}'.format(
info['deltachat_core_version'], info['deltachat_core_version'],
info['sqlite_version'], info['sqlite_version'],
info['journal_mode'],
)]) )])
cfg = config.option.liveconfig cfg = config.option.liveconfig
@@ -231,7 +230,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def make_account(self, path, logid, quiet=False): def make_account(self, path, logid, quiet=False):
ac = Account(path, logging=self._logging) ac = Account(path, logging=self._logging)
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac)) ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
ac._configtracker = ac.add_account_plugin(ConfigureTracker()) ac.addr = ac.get_self_contact().addr
if not quiet: if not quiet:
ac.add_account_plugin(FFIEventLogger(ac, logid=logid)) ac.add_account_plugin(FFIEventLogger(ac, logid=logid))
self._accounts.append(ac) self._accounts.append(ac)
@@ -302,24 +301,33 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
configdict["mvbox_move"] = str(int(move)) configdict["mvbox_move"] = str(int(move))
configdict["sentbox_watch"] = str(int(sentbox)) configdict["sentbox_watch"] = str(int(sentbox))
ac.update_config(configdict) ac.update_config(configdict)
ac.start() ac.configure()
return ac return ac
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False): def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
ac1 = self.get_online_configuring_account( ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move) pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
ac1._configtracker.wait_imap_connected() ac1.wait_configure_finish()
ac1._configtracker.wait_smtp_connected() ac1.start_io()
ac1._configtracker.wait_finish()
return ac1 return ac1
def get_two_online_accounts(self, move=False, quiet=False): def get_two_online_accounts(self, move=False, quiet=False):
ac1 = self.get_online_configuring_account(move=True, quiet=quiet) ac1 = self.get_online_configuring_account(move=True, quiet=quiet)
ac2 = self.get_online_configuring_account(quiet=quiet) ac2 = self.get_online_configuring_account(quiet=quiet)
ac1._configtracker.wait_finish() ac1.wait_configure_finish()
ac2._configtracker.wait_finish() ac1.start_io()
ac2.wait_configure_finish()
ac2.start_io()
return ac1, ac2 return ac1, ac2
def get_many_online_accounts(self, num, move=True, quiet=True):
accounts = [self.get_online_configuring_account(move=move, quiet=quiet)
for i in range(num)]
for acc in accounts:
acc._configtracker.wait_finish()
acc.start_io()
return accounts
def clone_online_account(self, account, pre_generated_key=True): def clone_online_account(self, account, pre_generated_key=True):
self.live_count += 1 self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count) tmpdb = tmpdir.join("livedb%d" % self.live_count)
@@ -335,7 +343,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
mvbox_move=account.get_config("mvbox_move"), mvbox_move=account.get_config("mvbox_move"),
sentbox_watch=account.get_config("sentbox_watch"), sentbox_watch=account.get_config("sentbox_watch"),
)) ))
ac.start() ac.configure()
return ac return ac
def run_bot_process(self, module, ffi=True): def run_bot_process(self, module, ffi=True):
@@ -392,6 +400,7 @@ class BotProcess:
break break
line = line.strip() line = line.strip()
self.stdout_queue.put(line) self.stdout_queue.put(line)
print("bot-stdout: ", line)
finally: finally:
self.stdout_queue.put(None) self.stdout_queue.put(None)

View File

@@ -18,7 +18,7 @@ class ImexTracker:
if ffi_event.name == "DC_EVENT_IMEX_PROGRESS": if ffi_event.name == "DC_EVENT_IMEX_PROGRESS":
self._imex_events.put(ffi_event.data1) self._imex_events.put(ffi_event.data1)
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN": elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
self._imex_events.put(ffi_event.data1) self._imex_events.put(ffi_event.data2)
def wait_finish(self, progress_timeout=60): def wait_finish(self, progress_timeout=60):
""" Return list of written files, raise ValueError if ExportFailed. """ """ Return list of written files, raise ValueError if ExportFailed. """
@@ -45,6 +45,7 @@ class ConfigureTracker:
self._smtp_finished = Event() self._smtp_finished = Event()
self._imap_finished = Event() self._imap_finished = Event()
self._ffi_events = [] self._ffi_events = []
self._progress = Queue()
@account_hookimpl @account_hookimpl
def ac_process_ffi_event(self, ffi_event): def ac_process_ffi_event(self, ffi_event):
@@ -53,6 +54,8 @@ class ConfigureTracker:
self._smtp_finished.set() self._smtp_finished.set()
elif ffi_event.name == "DC_EVENT_IMAP_CONNECTED": elif ffi_event.name == "DC_EVENT_IMAP_CONNECTED":
self._imap_finished.set() self._imap_finished.set()
elif ffi_event.name == "DC_EVENT_CONFIGURE_PROGRESS":
self._progress.put(ffi_event.data1)
@account_hookimpl @account_hookimpl
def ac_configure_completed(self, success): def ac_configure_completed(self, success):
@@ -66,6 +69,12 @@ class ConfigureTracker:
""" wait until smtp is configured. """ """ wait until smtp is configured. """
self._imap_finished.wait() self._imap_finished.wait()
def wait_progress(self, data1=None):
while 1:
evdata = self._progress.get()
if data1 is None or evdata == data1:
break
def wait_finish(self): def wait_finish(self):
""" wait until configure is completed. """ wait until configure is completed.

View File

@@ -10,4 +10,6 @@ if __name__ == "__main__":
for relpath in os.listdir(workspacedir): for relpath in os.listdir(workspacedir):
if relpath.startswith("deltachat"): if relpath.startswith("deltachat"):
p = os.path.join(workspacedir, relpath) p = os.path.join(workspacedir, relpath)
subprocess.check_call(["auditwheel", "repair", p, "-w", workspacedir]) subprocess.check_call(
["auditwheel", "repair", p, "-w", workspacedir,
"--plat", "manylinux2014_x86_64"])

View File

@@ -1,18 +0,0 @@
from __future__ import print_function
def wait_configuration_progress(account, min_target, max_target=1001):
min_target = min(min_target, max_target)
while 1:
event = account._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
if event.data1 >= min_target and event.data1 <= max_target:
print("** CONFIG PROGRESS {}".format(min_target), account)
break
def wait_securejoin_inviter_progress(account, target):
while 1:
event = account._evtracker.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
if event.data2 >= target:
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account)
break

View File

@@ -0,0 +1,135 @@
import time
import threading
import pytest
import os
from queue import Queue, Empty
import deltachat
def test_db_busy_error(acfactory, tmpdir):
starttime = time.time()
log_lock = threading.RLock()
def log(string):
with log_lock:
print("%3.2f %s" % (time.time() - starttime, string))
# make a number of accounts
accounts = acfactory.get_many_online_accounts(3, quiet=True)
log("created %s accounts" % len(accounts))
# put a bigfile into each account
for acc in accounts:
acc.bigfile = os.path.join(acc.get_blobdir(), "bigfile")
with open(acc.bigfile, "wb") as f:
f.write(b"01234567890"*1000_000)
log("created %s bigfiles" % len(accounts))
contact_addrs = [acc.get_self_contact().addr for acc in accounts]
chat = accounts[0].create_group_chat("stress-group")
for addr in contact_addrs[1:]:
chat.add_contact(chat.account.create_contact(addr))
# setup auto-responder bots which report back failures/actions
report_queue = Queue()
def report_func(replier, report_type, *report_args):
report_queue.put((replier, report_type, report_args))
# each replier receives all events and sends report events to receive_queue
repliers = []
for acc in accounts:
replier = AutoReplier(acc, log=log, num_send=500, num_bigfiles=5, report_func=report_func)
acc.add_account_plugin(replier)
repliers.append(replier)
# kick off message sending
# after which repliers will reply to each other
chat.send_text("hello")
alive_count = len(accounts)
while alive_count > 0:
try:
replier, report_type, report_args = report_queue.get(timeout=10)
except Empty:
log("timeout waiting for next event")
pytest.fail("timeout exceeded")
if report_type == ReportType.exit:
replier.log("EXIT")
elif report_type == ReportType.ffi_error:
replier.log("ERROR: {}".format(report_args[0]))
elif report_type == ReportType.message_echo:
continue
else:
raise ValueError("{} unknown report type {}, args={}".format(
addr, report_type, report_args
))
alive_count -= 1
replier.log("shutting down")
replier.account.shutdown()
replier.log("shut down complete, remaining={}".format(alive_count))
class ReportType:
exit = "exit"
ffi_error = "ffi-error"
message_echo = "message-echo"
class AutoReplier:
def __init__(self, account, log, num_send, num_bigfiles, report_func):
self.account = account
self._log = log
self.report_func = report_func
self.num_send = num_send
self.num_bigfiles = num_bigfiles
self.current_sent = 0
self.addr = self.account.get_self_contact().addr
self._thread = threading.Thread(
name="Stats{}".format(self.account),
target=self.thread_stats
)
self._thread.setDaemon(True)
self._thread.start()
def log(self, message):
self._log("{} {}".format(self.addr, message))
def thread_stats(self):
# XXX later use, for now we just quit
return
while 1:
time.sleep(1.0)
break
@deltachat.account_hookimpl
def ac_incoming_message(self, message):
if self.current_sent >= self.num_send:
self.report_func(self, ReportType.exit)
return
message.accept_sender_contact()
message.mark_seen()
self.log("incoming message: {}".format(message))
self.current_sent += 1
# we are still alive, let's send a reply
if self.num_bigfiles and self.current_sent % (self.num_send / self.num_bigfiles) == 0:
message.chat.send_text("send big file as reply to: {}".format(message.text))
msg = message.chat.send_file(self.account.bigfile)
else:
msg = message.chat.send_text("got message id {}, small text reply".format(message.id))
assert msg.text
self.log("message-sent: {}".format(msg))
self.report_func(self, ReportType.message_echo)
if self.current_sent >= self.num_send:
self.report_func(self, ReportType.exit)
return
@deltachat.account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self.log(ffi_event)
if ffi_event.name == "DC_EVENT_ERROR":
self.report_func(self, ReportType.ffi_error, ffi_event)

View File

@@ -7,8 +7,6 @@ from deltachat import const, Account
from deltachat.message import Message from deltachat.message import Message
from deltachat.hookspec import account_hookimpl from deltachat.hookspec import account_hookimpl
from datetime import datetime, timedelta from datetime import datetime, timedelta
from conftest import (wait_configuration_progress,
wait_securejoin_inviter_progress)
@pytest.mark.parametrize("msgtext,res", [ @pytest.mark.parametrize("msgtext,res", [
@@ -398,11 +396,9 @@ class TestOfflineChat:
with bin.open("w") as f: with bin.open("w") as f:
f.write("\00123" * 10000) f.write("\00123" * 10000)
msg = chat.send_file(bin.strpath) msg = chat.send_file(bin.strpath)
contact = msg.get_sender_contact() contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact() assert contact == ac1.get_self_contact()
assert not backupdir.listdir() assert not backupdir.listdir()
path = ac1.export_all(backupdir.strpath) path = ac1.export_all(backupdir.strpath)
assert os.path.exists(path) assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account() ac2 = acfactory.get_unconfigured_account()
@@ -475,8 +471,17 @@ class TestOfflineChat:
num_contacts = len(chat.get_contacts()) num_contacts = len(chat.get_contacts())
assert num_contacts == 11 assert num_contacts == 11
# perform plugin hooks # let's make sure the events perform plugin hooks
ac1._handle_current_events() def wait_events(cond):
now = time.time()
while time.time() < now + 5:
if cond():
break
time.sleep(0.1)
else:
pytest.fail("failed to get events")
wait_events(lambda: len(in_list) == 10)
assert len(in_list) == 10 assert len(in_list) == 10
chat_contacts = chat.get_contacts() chat_contacts = chat.get_contacts()
@@ -495,7 +500,7 @@ class TestOfflineChat:
chat.remove_contact(contacts[3]) chat.remove_contact(contacts[3])
assert len(chat.get_contacts()) == 9 assert len(chat.get_contacts()) == 9
ac1._handle_current_events() wait_events(lambda: len(in_list) == 2)
assert len(in_list) == 2 assert len(in_list) == 2
assert in_list[0][0] == "removed" assert in_list[0][0] == "removed"
assert in_list[0][1] == chat assert in_list[0][1] == chat
@@ -514,11 +519,6 @@ class TestOnlineAccount:
ac2.create_chat_by_contact(ac2.create_contact(email=ac1.get_config("addr"))) ac2.create_chat_by_contact(ac2.create_contact(email=ac1.get_config("addr")))
return chat return chat
def test_double_iter_events(self, acfactory):
ac1 = acfactory.get_one_online_account()
with pytest.raises(RuntimeError):
next(ac1.iter_events())
@pytest.mark.ignored @pytest.mark.ignored
def test_configure_generate_key(self, acfactory, lp): def test_configure_generate_key(self, acfactory, lp):
# A slow test which will generate new keys. # A slow test which will generate new keys.
@@ -532,8 +532,10 @@ class TestOnlineAccount:
) )
# rsa key gen can be slow especially on CI, adjust timeout # rsa key gen can be slow especially on CI, adjust timeout
ac1._evtracker.set_timeout(120) ac1._evtracker.set_timeout(120)
wait_configuration_progress(ac1, 1000) ac1.wait_configure_finish()
wait_configuration_progress(ac2, 1000) ac1.start_io()
ac2.wait_configure_finish()
ac2.start_io()
chat = self.get_chat(ac1, ac2, both_created=True) chat = self.get_chat(ac1, ac2, both_created=True)
lp.sec("ac1: send unencrypted message to ac2") lp.sec("ac1: send unencrypted message to ac2")
@@ -562,12 +564,16 @@ class TestOnlineAccount:
def test_configure_canceled(self, acfactory): def test_configure_canceled(self, acfactory):
ac1 = acfactory.get_online_configuring_account() ac1 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 200) ac1._configtracker.wait_progress()
ac1.stop_ongoing() ac1.stop_ongoing()
wait_configuration_progress(ac1, 0, 0) try:
ac1.wait_configure_finish()
except Exception:
pass
def test_export_import_self_keys(self, acfactory, tmpdir): def test_export_import_self_keys(self, acfactory, tmpdir):
ac1, ac2 = acfactory.get_two_online_accounts() ac1, ac2 = acfactory.get_two_online_accounts()
dir = tmpdir.mkdir("exportdir") dir = tmpdir.mkdir("exportdir")
export_files = ac1.export_self_keys(dir.strpath) export_files = ac1.export_self_keys(dir.strpath)
assert len(export_files) == 2 assert len(export_files) == 2
@@ -584,9 +590,12 @@ class TestOnlineAccount:
# are copied to it via BCC. # are copied to it via BCC.
ac1_clone = acfactory.clone_online_account(ac1) ac1_clone = acfactory.clone_online_account(ac1)
wait_configuration_progress(ac1, 1000) ac1.wait_configure_finish()
wait_configuration_progress(ac2, 1000) ac1.start_io()
wait_configuration_progress(ac1_clone, 1000) ac2.wait_configure_finish()
ac2.start_io()
ac1_clone.wait_configure_finish()
ac1_clone.start_io()
chat = self.get_chat(ac1, ac2) chat = self.get_chat(ac1, ac2)
@@ -690,10 +699,12 @@ class TestOnlineAccount:
ac2 = acfactory.get_online_configuring_account() ac2 = acfactory.get_online_configuring_account()
lp.sec("ac2: waiting for configuration") lp.sec("ac2: waiting for configuration")
wait_configuration_progress(ac2, 1000) ac2.wait_configure_finish()
ac2.start_io()
lp.sec("ac1: waiting for configuration") lp.sec("ac1: waiting for configuration")
wait_configuration_progress(ac1, 1000) ac1.wait_configure_finish()
ac1.start_io()
lp.sec("ac1: send message and wait for ac2 to receive it") lp.sec("ac1: send message and wait for ac2 to receive it")
chat = self.get_chat(ac1, ac2) chat = self.get_chat(ac1, ac2)
@@ -705,8 +716,10 @@ class TestOnlineAccount:
def test_move_works(self, acfactory): def test_move_works(self, acfactory):
ac1 = acfactory.get_online_configuring_account() ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account(mvbox=True, move=True) ac2 = acfactory.get_online_configuring_account(mvbox=True, move=True)
wait_configuration_progress(ac2, 1000) ac2.wait_configure_finish()
wait_configuration_progress(ac1, 1000) ac2.start_io()
ac1.wait_configure_finish()
ac1.start_io()
chat = self.get_chat(ac1, ac2) chat = self.get_chat(ac1, ac2)
chat.send_text("message1") chat.send_text("message1")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
@@ -717,8 +730,11 @@ class TestOnlineAccount:
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True) ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True)
ac1.set_config("bcc_self", "1") ac1.set_config("bcc_self", "1")
ac2 = acfactory.get_online_configuring_account() ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac2, 1000) ac2.wait_configure_finish()
wait_configuration_progress(ac1, 1000) ac2.start_io()
ac1.wait_configure_finish()
ac1.start_io()
chat = self.get_chat(ac1, ac2) chat = self.get_chat(ac1, ac2)
chat.send_text("message1") chat.send_text("message1")
chat.send_text("message2") chat.send_text("message2")
@@ -793,8 +809,9 @@ class TestOnlineAccount:
ac1.empty_server_folders(inbox=True, mvbox=True) ac1.empty_server_folders(inbox=True, mvbox=True)
ev1 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED") ev1 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED")
ev2 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED") ev2 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED")
boxes = sorted([ev1.data2, ev2.data2]) boxes = [ev1.data2, ev2.data2]
assert boxes == ["DeltaChat", "INBOX"] boxes.remove("INBOX")
assert len(boxes) == 1 and boxes[0].endswith("DeltaChat")
def test_send_and_receive_message_markseen(self, acfactory, lp): def test_send_and_receive_message_markseen(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts() ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1047,6 +1064,35 @@ class TestOnlineAccount:
assert mime.get_all("From") assert mime.get_all("From")
assert mime.get_all("Received") assert mime.get_all("Received")
def test_send_mark_seen_clean_incoming_events(self, acfactory, lp, data):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = self.get_chat(ac1, ac2, both_created=True)
message_queue = queue.Queue()
class InPlugin:
@account_hookimpl
def ac_incoming_message(self, message):
message_queue.put(message)
ac1.add_account_plugin(InPlugin())
lp.sec("sending one message from ac1 to ac2")
chat.send_text("hello")
lp.sec("ac2: waiting to receive")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
lp.sec("ac2: mark seen {}".format(msg))
msg.mark_seen()
for ev in ac1._evtracker.iter_events():
if ev.name == "DC_EVENT_INCOMING_MSG":
pytest.fail("MDN arrived as regular incoming message")
elif ev.name == "DC_EVENT_MSG_READ":
break
def test_send_and_receive_image(self, acfactory, lp, data): def test_send_and_receive_image(self, acfactory, lp, data):
ac1, ac2 = acfactory.get_two_online_accounts() ac1, ac2 = acfactory.get_two_online_accounts()
chat = self.get_chat(ac1, ac2) chat = self.get_chat(ac1, ac2)
@@ -1096,8 +1142,7 @@ class TestOnlineAccount:
assert m == msg_in assert m == msg_in
def test_import_export_online_all(self, acfactory, tmpdir, lp): def test_import_export_online_all(self, acfactory, tmpdir, lp):
ac1 = acfactory.get_online_configuring_account() ac1 = acfactory.get_one_online_account()
wait_configuration_progress(ac1, 1000)
lp.sec("create some chat content") lp.sec("create some chat content")
contact1 = ac1.create_contact("some1@hello.com", name="some1") contact1 = ac1.create_contact("some1@hello.com", name="some1")
@@ -1145,8 +1190,11 @@ class TestOnlineAccount:
# as of Jul2019 # as of Jul2019
ac1 = acfactory.get_online_configuring_account() ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.clone_online_account(ac1) ac2 = acfactory.clone_online_account(ac1)
wait_configuration_progress(ac2, 1000) ac2.wait_configure_finish()
wait_configuration_progress(ac1, 1000) ac2.start_io()
ac1.wait_configure_finish()
ac1.start_io()
lp.sec("trigger ac setup message and return setupcode") lp.sec("trigger ac setup message and return setupcode")
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"] assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
setup_code = ac1.initiate_key_transfer() setup_code = ac1.initiate_key_transfer()
@@ -1168,8 +1216,10 @@ class TestOnlineAccount:
ac1 = acfactory.get_online_configuring_account() ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.clone_online_account(ac1) ac2 = acfactory.clone_online_account(ac1)
ac2._evtracker.set_timeout(30) ac2._evtracker.set_timeout(30)
wait_configuration_progress(ac2, 1000) ac2.wait_configure_finish()
wait_configuration_progress(ac1, 1000) ac2.start_io()
ac1.wait_configure_finish()
ac1.start_io()
lp.sec("trigger ac setup message but ignore") lp.sec("trigger ac setup message but ignore")
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"] assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
@@ -1195,7 +1245,7 @@ class TestOnlineAccount:
lp.sec("ac2: start QR-code based setup contact protocol") lp.sec("ac2: start QR-code based setup contact protocol")
ch = ac2.qr_setup_contact(qr) ch = ac2.qr_setup_contact(qr)
assert ch.id >= 10 assert ch.id >= 10
wait_securejoin_inviter_progress(ac1, 1000) ac1._evtracker.wait_securejoin_inviter_progress(1000)
def test_qr_join_chat(self, acfactory, lp): def test_qr_join_chat(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts() ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1204,11 +1254,12 @@ class TestOnlineAccount:
qr = chat.get_join_qr() qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol") lp.sec("ac2: start QR-code based join-group protocol")
ch = ac2.qr_join_chat(qr) ch = ac2.qr_join_chat(qr)
lp.sec("ac2: qr_join_chat() returned")
assert ch.id >= 10 assert ch.id >= 10
# check that at least some of the handshake messages are deleted # check that at least some of the handshake messages are deleted
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED") ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED") ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
wait_securejoin_inviter_progress(ac1, 1000) ac1._evtracker.wait_securejoin_inviter_progress(1000)
def test_qr_verified_group_and_chatting(self, acfactory, lp): def test_qr_verified_group_and_chatting(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts() ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1219,7 +1270,7 @@ class TestOnlineAccount:
lp.sec("ac2: start QR-code based join-group protocol") lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr) chat2 = ac2.qr_join_chat(qr)
assert chat2.id >= 10 assert chat2.id >= 10
wait_securejoin_inviter_progress(ac1, 1000) ac1._evtracker.wait_securejoin_inviter_progress(1000)
lp.sec("ac2: read member added message") lp.sec("ac2: read member added message")
msg = ac2._evtracker.wait_next_incoming_message() msg = ac2._evtracker.wait_next_incoming_message()
@@ -1479,7 +1530,8 @@ class TestGroupStressTests:
lp.sec("creating and configuring five accounts") lp.sec("creating and configuring five accounts")
accounts = [acfactory.get_online_configuring_account() for i in range(5)] accounts = [acfactory.get_online_configuring_account() for i in range(5)]
for acc in accounts: for acc in accounts:
wait_configuration_progress(acc, 1000) acc.wait_configure_finish()
acc.start_io()
ac1 = accounts.pop() ac1 = accounts.pop()
lp.sec("ac1: setting up contacts with 4 other members") lp.sec("ac1: setting up contacts with 4 other members")
@@ -1572,6 +1624,9 @@ class TestGroupStressTests:
# Message should be encrypted because keys of other members are gossiped # Message should be encrypted because keys of other members are gossiped
assert msg.is_encrypted() assert msg.is_encrypted()
for account in accounts:
account.shutdown()
def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp): def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp):
""" """
Test that user recreates group member list when it joins the group again. Test that user recreates group member list when it joins the group again.
@@ -1583,7 +1638,8 @@ class TestGroupStressTests:
lp.sec("creating and configuring five accounts") lp.sec("creating and configuring five accounts")
accounts = [acfactory.get_online_configuring_account() for i in range(3)] accounts = [acfactory.get_online_configuring_account() for i in range(3)]
for acc in accounts: for acc in accounts:
wait_configuration_progress(acc, 1000) acc.wait_configure_finish()
acc.start_io()
ac1 = accounts.pop() ac1 = accounts.pop()
lp.sec("ac1: setting up contacts with 2 other members") lp.sec("ac1: setting up contacts with 2 other members")
@@ -1646,31 +1702,35 @@ class TestGroupStressTests:
assert len(msg.chat.get_contacts()) == len(chat.get_contacts()) assert len(msg.chat.get_contacts()) == len(chat.get_contacts())
ac1.shutdown()
ac2.shutdown()
ac3.shutdown()
class TestOnlineConfigureFails: class TestOnlineConfigureFails:
def test_invalid_password(self, acfactory): def test_invalid_password(self, acfactory):
ac1, configdict = acfactory.get_online_config() ac1, configdict = acfactory.get_online_config()
ac1.update_config(dict(addr=configdict["addr"], mail_pw="123")) ac1.update_config(dict(addr=configdict["addr"], mail_pw="123"))
ac1.start() ac1.configure()
wait_configuration_progress(ac1, 500) ac1._configtracker.wait_progress(500)
ac1._configtracker.wait_progress(0)
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK") ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "cannot login" in ev.data2.lower() assert "cannot login" in ev.data2.lower()
wait_configuration_progress(ac1, 0, 0)
def test_invalid_user(self, acfactory): def test_invalid_user(self, acfactory):
ac1, configdict = acfactory.get_online_config() ac1, configdict = acfactory.get_online_config()
ac1.update_config(dict(addr="x" + configdict["addr"], mail_pw=configdict["mail_pw"])) ac1.update_config(dict(addr="x" + configdict["addr"], mail_pw=configdict["mail_pw"]))
ac1.start() ac1.configure()
wait_configuration_progress(ac1, 500) ac1._configtracker.wait_progress(500)
ac1._configtracker.wait_progress(0)
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK") ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "cannot login" in ev.data2.lower() assert "cannot login" in ev.data2.lower()
wait_configuration_progress(ac1, 0, 0)
def test_invalid_domain(self, acfactory): def test_invalid_domain(self, acfactory):
ac1, configdict = acfactory.get_online_config() ac1, configdict = acfactory.get_online_config()
ac1.update_config((dict(addr=configdict["addr"] + "x", mail_pw=configdict["mail_pw"]))) ac1.update_config((dict(addr=configdict["addr"] + "x", mail_pw=configdict["mail_pw"])))
ac1.start() ac1.configure()
wait_configuration_progress(ac1, 500) ac1._configtracker.wait_progress(500)
ac1._configtracker.wait_progress(0)
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK") ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "could not connect" in ev.data2.lower() assert "could not connect" in ev.data2.lower()
wait_configuration_progress(ac1, 0, 0)

View File

@@ -6,24 +6,36 @@ import shutil
import pytest import pytest
from filecmp import cmp from filecmp import cmp
from conftest import wait_configuration_progress
from deltachat import const from deltachat import const
def wait_msgs_changed(account, chat_id, msg_id=None): def wait_msg_delivered(account, msg_list):
ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED") """ wait for one or more MSG_DELIVERED events to match msg_list contents. """
assert ev.data1 == chat_id msg_list = list(msg_list)
if msg_id is not None: while msg_list:
assert ev.data2 == msg_id ev = account._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
return ev.data2 msg_list.remove((ev.data1, ev.data2))
def wait_msgs_changed(account, msgs_list):
""" wait for one or more MSGS_CHANGED events to match msgs_list contents. """
account.log("waiting for msgs_list={}".format(msgs_list))
msgs_list = list(msgs_list)
while msgs_list:
ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
for i, (data1, data2) in enumerate(msgs_list):
if ev.data1 == data1:
if data2 is None or ev.data2 == data2:
del msgs_list[i]
break
else:
account.log("waiting mismatch data1={} data2={}".format(data1, data2))
return ev.data1, ev.data2
class TestOnlineInCreation: class TestOnlineInCreation:
def test_increation_not_blobdir(self, tmpdir, acfactory, lp): def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
ac1 = acfactory.get_online_configuring_account() ac1, ac2 = acfactory.get_two_online_accounts()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
c2 = ac1.create_contact(email=ac2.get_config("addr")) c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2) chat = ac1.create_chat_by_contact(c2)
@@ -35,10 +47,7 @@ class TestOnlineInCreation:
chat.prepare_message_file(src.strpath) chat.prepare_message_file(src.strpath)
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp): def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
ac1 = acfactory.get_online_configuring_account() ac1, ac2 = acfactory.get_two_online_accounts()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
c2 = ac1.create_contact(email=ac2.get_config("addr")) c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2) chat = ac1.create_chat_by_contact(c2)
@@ -53,15 +62,12 @@ class TestOnlineInCreation:
assert os.path.exists(blob_src), "file.txt not copied to blobdir" assert os.path.exists(blob_src), "file.txt not copied to blobdir"
def test_forward_increation(self, acfactory, data, lp): def test_forward_increation(self, acfactory, data, lp):
ac1 = acfactory.get_online_configuring_account() ac1, ac2 = acfactory.get_two_online_accounts()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
c2 = ac1.create_contact(email=ac2.get_config("addr")) c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2) chat = ac1.create_chat_by_contact(c2)
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
wait_msgs_changed(ac1, 0, 0) # why no chat id? wait_msgs_changed(ac1, [(0, 0)]) # why no chat id?
lp.sec("create a message with a file in creation") lp.sec("create a message with a file in creation")
orig = data.get_path("d.png") orig = data.get_path("d.png")
@@ -70,19 +76,16 @@ class TestOnlineInCreation:
fp.write("preparing") fp.write("preparing")
prepared_original = chat.prepare_message_file(path) prepared_original = chat.prepare_message_file(path)
assert prepared_original.is_out_preparing() assert prepared_original.is_out_preparing()
wait_msgs_changed(ac1, chat.id, prepared_original.id) wait_msgs_changed(ac1, [(chat.id, prepared_original.id)])
lp.sec("forward the message while still in creation") lp.sec("forward the message while still in creation")
chat2 = ac1.create_group_chat("newgroup") chat2 = ac1.create_group_chat("newgroup")
chat2.add_contact(c2) chat2.add_contact(c2)
wait_msgs_changed(ac1, 0, 0) # why not chat id? wait_msgs_changed(ac1, [(0, 0)]) # why not chat id?
ac1.forward_messages([prepared_original], chat2) ac1.forward_messages([prepared_original], chat2)
# XXX there might be two EVENT_MSGS_CHANGED and only one of them # XXX there might be two EVENT_MSGS_CHANGED and only one of them
# is the one caused by forwarding # is the one caused by forwarding
forwarded_id = wait_msgs_changed(ac1, chat2.id) _, forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
if forwarded_id == 0:
forwarded_id = wait_msgs_changed(ac1, chat2.id)
assert forwarded_id
forwarded_msg = ac1.get_message_by_id(forwarded_id) forwarded_msg = ac1.get_message_by_id(forwarded_id)
assert forwarded_msg.is_out_preparing() assert forwarded_msg.is_out_preparing()
@@ -91,20 +94,18 @@ class TestOnlineInCreation:
shutil.copyfile(orig, path) shutil.copyfile(orig, path)
chat.send_prepared(prepared_original) chat.send_prepared(prepared_original)
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered() assert prepared_original.is_out_pending() or prepared_original.is_out_delivered()
wait_msgs_changed(ac1, chat.id, prepared_original.id)
lp.sec("expect the forwarded message to be sent now too") lp.sec("check that both forwarded and original message are proper.")
wait_msgs_changed(ac1, chat2.id, forwarded_id) wait_msgs_changed(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)])
fwd_msg = ac1.get_message_by_id(forwarded_id) fwd_msg = ac1.get_message_by_id(forwarded_id)
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered() assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
lp.sec("wait for the messages to be delivered to SMTP") lp.sec("wait for both messages to be delivered to SMTP")
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED") wait_msg_delivered(ac1, [
assert ev.data1 == chat.id (chat2.id, forwarded_id),
assert ev.data2 == prepared_original.id (chat.id, prepared_original.id)
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED") ])
assert ev.data1 == chat2.id
assert ev.data2 == forwarded_id
lp.sec("wait1 for original or forwarded messages to arrive") lp.sec("wait1 for original or forwarded messages to arrive")
ev1 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED") ev1 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")

View File

@@ -1,75 +1,52 @@
from __future__ import print_function from __future__ import print_function
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
from queue import Queue
from deltachat import capi, cutil, const
from deltachat import register_global_plugin from deltachat import register_global_plugin
from deltachat.hookspec import global_hookimpl from deltachat.hookspec import global_hookimpl
from deltachat.capi import ffi from deltachat.capi import ffi
from deltachat.capi import lib from deltachat.capi import lib
# from deltachat.account import EventLogger
def test_empty_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.ffi.NULL)
capi.lib.dc_close(ctx) capi.lib.dc_context_unref(ctx)
def test_callback_None2int():
ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL)
set_context_callback(ctx, lambda *args: None)
capi.lib.dc_close(ctx)
clear_context_callback(ctx)
def test_dc_close_events(tmpdir, acfactory): def test_dc_close_events(tmpdir, acfactory):
ac1 = acfactory.get_unconfigured_account() ac1 = acfactory.get_unconfigured_account()
# register after_shutdown function # register after_shutdown function
shutdowns = [] shutdowns = Queue()
class ShutdownPlugin: class ShutdownPlugin:
@global_hookimpl @global_hookimpl
def dc_account_after_shutdown(self, account): def dc_account_after_shutdown(self, account):
assert account._dc_context is None assert account._dc_context is None
shutdowns.append(account) shutdowns.put(account)
register_global_plugin(ShutdownPlugin()) register_global_plugin(ShutdownPlugin())
assert hasattr(ac1, "_dc_context") assert hasattr(ac1, "_dc_context")
ac1.shutdown() ac1.shutdown()
assert shutdowns == [ac1] shutdowns.get(timeout=2)
def find(info_string):
evlog = ac1._evtracker
while 1:
ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
data2 = ev.data2
if info_string in data2:
return
else:
print("skipping event", ev)
find("disconnecting inbox-thread")
find("disconnecting sentbox-thread")
find("disconnecting mvbox-thread")
find("disconnecting SMTP")
find("Database closed")
def test_wrong_db(tmpdir): def test_wrong_db(tmpdir):
dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
p = tmpdir.join("hello.db") p = tmpdir.join("hello.db")
# write an invalid database file # write an invalid database file
p.write("x123" * 10) p.write("x123" * 10)
assert not lib.dc_open(dc_context, p.strpath.encode("ascii"), ffi.NULL)
assert ffi.NULL == lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
def test_empty_blobdir(tmpdir): def test_empty_blobdir(tmpdir):
db_fname = tmpdir.join("hello.db")
# Apparently some client code expects this to be the same as passing NULL. # Apparently some client code expects this to be the same as passing NULL.
ctx = ffi.gc( ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL), lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""),
lib.dc_context_unref, lib.dc_context_unref,
) )
db_fname = tmpdir.join("hello.db") assert ctx != ffi.NULL
assert lib.dc_open(ctx, db_fname.strpath.encode("ascii"), b"")
def test_event_defines(): def test_event_defines():
@@ -78,17 +55,20 @@ def test_event_defines():
def test_sig(): def test_sig():
sig = capi.lib.dc_get_event_signature_types sig = capi.lib.dc_event_has_string_data
assert sig(const.DC_EVENT_INFO) == 2 assert not sig(const.DC_EVENT_MSGS_CHANGED)
assert sig(const.DC_EVENT_WARNING) == 2 assert sig(const.DC_EVENT_INFO)
assert sig(const.DC_EVENT_ERROR) == 2 assert sig(const.DC_EVENT_WARNING)
assert sig(const.DC_EVENT_SMTP_CONNECTED) == 2 assert sig(const.DC_EVENT_ERROR)
assert sig(const.DC_EVENT_IMAP_CONNECTED) == 2 assert sig(const.DC_EVENT_SMTP_CONNECTED)
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT) == 2 assert sig(const.DC_EVENT_IMAP_CONNECTED)
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
def test_markseen_invalid_message_ids(acfactory): def test_markseen_invalid_message_ids(acfactory):
ac1 = acfactory.get_configured_offline_account() ac1 = acfactory.get_configured_offline_account()
contact1 = ac1.create_contact(email="some1@example.com", name="some1") contact1 = ac1.create_contact(email="some1@example.com", name="some1")
chat = ac1.create_chat_by_contact(contact1) chat = ac1.create_chat_by_contact(contact1)
chat.send_text("one messae") chat.send_text("one messae")
@@ -107,47 +87,18 @@ def test_get_special_message_id_returns_empty_message(acfactory):
def test_provider_info_none(): def test_provider_info_none():
ctx = ffi.gc( ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL), lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
lib.dc_context_unref, 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(ctx, 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_unref,
)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
assert 'deltachat_core_version' in info
assert 'database_dir' not in info
def test_get_info_open(tmpdir): def test_get_info_open(tmpdir):
db_fname = tmpdir.join("test.db")
ctx = ffi.gc( ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL), lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
lib.dc_context_unref, lib.dc_context_unref,
) )
db_fname = tmpdir.join("test.db")
lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx)) info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
assert 'deltachat_core_version' in info assert 'deltachat_core_version' in info
assert 'database_dir' in info assert 'database_dir' in info
def test_is_open_closed():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_is_open(ctx) == 0
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_unref,
)
db_fname = tmpdir.join("test.db")
lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL)
assert lib.dc_is_open(ctx) == 1

View File

@@ -1 +1 @@
nightly-2020-03-12 1.43.1

View File

@@ -2,9 +2,10 @@
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fmt; use std::fmt;
use std::fs;
use std::io::Write; use async_std::path::{Path, PathBuf};
use std::path::{Path, PathBuf}; use async_std::prelude::*;
use async_std::{fs, io};
use image::GenericImageView; use image::GenericImageView;
use thiserror::Error; use thiserror::Error;
@@ -43,15 +44,16 @@ impl<'a> BlobObject<'a> {
/// [BlobError::WriteFailure] is used when the file could not /// [BlobError::WriteFailure] is used when the file could not
/// be written to. You can expect [BlobError.cause] to contain an /// be written to. You can expect [BlobError.cause] to contain an
/// underlying error. /// underlying error.
pub fn create( pub async fn create(
context: &'a Context, context: &'a Context,
suggested_name: impl AsRef<str>, suggested_name: impl AsRef<str>,
data: &[u8], data: &[u8],
) -> std::result::Result<BlobObject<'a>, BlobError> { ) -> std::result::Result<BlobObject<'a>, BlobError> {
let blobdir = context.get_blobdir(); let blobdir = context.get_blobdir();
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref()); let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext)?; let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext).await?;
file.write_all(data) file.write_all(data)
.await
.map_err(|err| BlobError::WriteFailure { .map_err(|err| BlobError::WriteFailure {
blobdir: blobdir.to_path_buf(), blobdir: blobdir.to_path_buf(),
blobname: name.clone(), blobname: name.clone(),
@@ -61,12 +63,16 @@ impl<'a> BlobObject<'a> {
blobdir, blobdir,
name: format!("$BLOBDIR/{}", name), name: format!("$BLOBDIR/{}", name),
}; };
context.call_cb(Event::NewBlobFile(blob.as_name().to_string())); context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob) Ok(blob)
} }
// Creates a new file, returning a tuple of the name and the handle. // Creates a new file, returning a tuple of the name and the handle.
fn create_new_file(dir: &Path, stem: &str, ext: &str) -> Result<(String, fs::File), BlobError> { async fn create_new_file(
dir: &Path,
stem: &str,
ext: &str,
) -> Result<(String, fs::File), BlobError> {
let max_attempt = 15; let max_attempt = 15;
let mut name = format!("{}{}", stem, ext); let mut name = format!("{}{}", stem, ext);
for attempt in 0..max_attempt { for attempt in 0..max_attempt {
@@ -75,6 +81,7 @@ impl<'a> BlobObject<'a> {
.create_new(true) .create_new(true)
.write(true) .write(true)
.open(&path) .open(&path)
.await
{ {
Ok(file) => return Ok((name, file)), Ok(file) => return Ok((name, file)),
Err(err) => { Err(err) => {
@@ -110,37 +117,41 @@ impl<'a> BlobObject<'a> {
/// In addition to the errors in [BlobObject::create] the /// In addition to the errors in [BlobObject::create] the
/// [BlobError::CopyFailure] is used when the data can not be /// [BlobError::CopyFailure] is used when the data can not be
/// copied. /// copied.
pub fn create_and_copy( pub async fn create_and_copy(
context: &'a Context, context: &'a Context,
src: impl AsRef<Path>, src: impl AsRef<Path>,
) -> std::result::Result<BlobObject<'a>, BlobError> { ) -> std::result::Result<BlobObject<'a>, BlobError> {
let mut src_file = fs::File::open(src.as_ref()).map_err(|err| BlobError::CopyFailure { let mut src_file =
blobdir: context.get_blobdir().to_path_buf(), fs::File::open(src.as_ref())
blobname: String::from(""), .await
src: src.as_ref().to_path_buf(), .map_err(|err| BlobError::CopyFailure {
cause: err, blobdir: context.get_blobdir().to_path_buf(),
})?; blobname: String::from(""),
src: src.as_ref().to_path_buf(),
cause: err,
})?;
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy()); let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
let (name, mut dst_file) = BlobObject::create_new_file(context.get_blobdir(), &stem, &ext)?; let (name, mut dst_file) =
BlobObject::create_new_file(context.get_blobdir(), &stem, &ext).await?;
let name_for_err = name.clone(); let name_for_err = name.clone();
std::io::copy(&mut src_file, &mut dst_file).map_err(|err| { if let Err(err) = io::copy(&mut src_file, &mut dst_file).await {
{ {
// Attempt to remove the failed file, swallow errors resulting from that. // Attempt to remove the failed file, swallow errors resulting from that.
let path = context.get_blobdir().join(&name_for_err); let path = context.get_blobdir().join(&name_for_err);
fs::remove_file(path).ok(); fs::remove_file(path).await.ok();
} }
BlobError::CopyFailure { return Err(BlobError::CopyFailure {
blobdir: context.get_blobdir().to_path_buf(), blobdir: context.get_blobdir().to_path_buf(),
blobname: name_for_err, blobname: name_for_err,
src: src.as_ref().to_path_buf(), src: src.as_ref().to_path_buf(),
cause: err, cause: err,
} });
})?; }
let blob = BlobObject { let blob = BlobObject {
blobdir: context.get_blobdir(), blobdir: context.get_blobdir(),
name: format!("$BLOBDIR/{}", name), name: format!("$BLOBDIR/{}", name),
}; };
context.call_cb(Event::NewBlobFile(blob.as_name().to_string())); context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob) Ok(blob)
} }
@@ -158,14 +169,14 @@ impl<'a> BlobObject<'a> {
/// This merely delegates to the [BlobObject::create_and_copy] and /// This merely delegates to the [BlobObject::create_and_copy] and
/// the [BlobObject::from_path] methods. See those for possible /// the [BlobObject::from_path] methods. See those for possible
/// errors. /// errors.
pub fn new_from_path( pub async fn new_from_path(
context: &Context, context: &Context,
src: impl AsRef<Path>, src: impl AsRef<Path>,
) -> std::result::Result<BlobObject, BlobError> { ) -> std::result::Result<BlobObject<'_>, BlobError> {
if src.as_ref().starts_with(context.get_blobdir()) { if src.as_ref().starts_with(context.get_blobdir()) {
BlobObject::from_path(context, src) BlobObject::from_path(context, src)
} else { } else {
BlobObject::create_and_copy(context, src) BlobObject::create_and_copy(context, src).await
} }
} }
@@ -418,58 +429,71 @@ mod tests {
use crate::test_utils::*; use crate::test_utils::*;
#[test] #[async_std::test]
fn test_create() { async fn test_create() {
let t = dummy_context(); let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo", b"hello").unwrap(); let blob = BlobObject::create(&t.ctx, "foo", b"hello").await.unwrap();
let fname = t.ctx.get_blobdir().join("foo"); let fname = t.ctx.get_blobdir().join("foo");
let data = fs::read(fname).unwrap(); let data = fs::read(fname).await.unwrap();
assert_eq!(data, b"hello"); assert_eq!(data, b"hello");
assert_eq!(blob.as_name(), "$BLOBDIR/foo"); assert_eq!(blob.as_name(), "$BLOBDIR/foo");
assert_eq!(blob.to_abs_path(), t.ctx.get_blobdir().join("foo")); assert_eq!(blob.to_abs_path(), t.ctx.get_blobdir().join("foo"));
} }
#[test] #[async_std::test]
fn test_lowercase_ext() { async fn test_lowercase_ext() {
let t = dummy_context(); let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello").unwrap(); let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello")
.await
.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt"); assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
} }
#[test] #[async_std::test]
fn test_as_file_name() { async fn test_as_file_name() {
let t = dummy_context(); let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap(); let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
assert_eq!(blob.as_file_name(), "foo.txt"); assert_eq!(blob.as_file_name(), "foo.txt");
} }
#[test] #[async_std::test]
fn test_as_rel_path() { async fn test_as_rel_path() {
let t = dummy_context(); let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap(); let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
assert_eq!(blob.as_rel_path(), Path::new("foo.txt")); assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
} }
#[test] #[async_std::test]
fn test_suffix() { async fn test_suffix() {
let t = dummy_context(); let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap(); let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
assert_eq!(blob.suffix(), Some("txt")); assert_eq!(blob.suffix(), Some("txt"));
let blob = BlobObject::create(&t.ctx, "bar", b"world").unwrap(); let blob = BlobObject::create(&t.ctx, "bar", b"world").await.unwrap();
assert_eq!(blob.suffix(), None); assert_eq!(blob.suffix(), None);
} }
#[test] #[async_std::test]
fn test_create_dup() { async fn test_create_dup() {
let t = dummy_context(); let t = dummy_context().await;
BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap(); BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
let foo_path = t.ctx.get_blobdir().join("foo.txt"); let foo_path = t.ctx.get_blobdir().join("foo.txt");
assert!(foo_path.exists()); assert!(foo_path.exists().await);
BlobObject::create(&t.ctx, "foo.txt", b"world").unwrap(); BlobObject::create(&t.ctx, "foo.txt", b"world")
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() { .await
.unwrap();
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name(); let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() { if fname == foo_path.file_name().unwrap() {
assert_eq!(fs::read(&foo_path).unwrap(), b"hello"); assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
} else { } else {
let name = fname.to_str().unwrap(); let name = fname.to_str().unwrap();
assert!(name.starts_with("foo")); assert!(name.starts_with("foo"));
@@ -478,17 +502,22 @@ mod tests {
} }
} }
#[test] #[async_std::test]
fn test_double_ext_preserved() { async fn test_double_ext_preserved() {
let t = dummy_context(); let t = dummy_context().await;
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello").unwrap(); BlobObject::create(&t.ctx, "foo.tar.gz", b"hello")
.await
.unwrap();
let foo_path = t.ctx.get_blobdir().join("foo.tar.gz"); let foo_path = t.ctx.get_blobdir().join("foo.tar.gz");
assert!(foo_path.exists()); assert!(foo_path.exists().await);
BlobObject::create(&t.ctx, "foo.tar.gz", b"world").unwrap(); BlobObject::create(&t.ctx, "foo.tar.gz", b"world")
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() { .await
.unwrap();
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name(); let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() { if fname == foo_path.file_name().unwrap() {
assert_eq!(fs::read(&foo_path).unwrap(), b"hello"); assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
} else { } else {
let name = fname.to_str().unwrap(); let name = fname.to_str().unwrap();
println!("{}", name); println!("{}", name);
@@ -498,55 +527,55 @@ mod tests {
} }
} }
#[test] #[async_std::test]
fn test_create_long_names() { async fn test_create_long_names() {
let t = dummy_context(); let t = dummy_context().await;
let s = "1".repeat(150); let s = "1".repeat(150);
let blob = BlobObject::create(&t.ctx, &s, b"data").unwrap(); let blob = BlobObject::create(&t.ctx, &s, b"data").await.unwrap();
let blobname = blob.as_name().split('/').last().unwrap(); let blobname = blob.as_name().split('/').last().unwrap();
assert!(blobname.len() < 128); assert!(blobname.len() < 128);
} }
#[test] #[async_std::test]
fn test_create_and_copy() { async fn test_create_and_copy() {
let t = dummy_context(); let t = dummy_context().await;
let src = t.dir.path().join("src"); let src = t.dir.path().join("src");
fs::write(&src, b"boo").unwrap(); fs::write(&src, b"boo").await.unwrap();
let blob = BlobObject::create_and_copy(&t.ctx, &src).unwrap(); let blob = BlobObject::create_and_copy(&t.ctx, &src).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/src"); assert_eq!(blob.as_name(), "$BLOBDIR/src");
let data = fs::read(blob.to_abs_path()).unwrap(); let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo"); assert_eq!(data, b"boo");
let whoops = t.dir.path().join("whoops"); let whoops = t.dir.path().join("whoops");
assert!(BlobObject::create_and_copy(&t.ctx, &whoops).is_err()); assert!(BlobObject::create_and_copy(&t.ctx, &whoops).await.is_err());
let whoops = t.ctx.get_blobdir().join("whoops"); let whoops = t.ctx.get_blobdir().join("whoops");
assert!(!whoops.exists()); assert!(!whoops.exists().await);
} }
#[test] #[async_std::test]
fn test_create_from_path() { async fn test_create_from_path() {
let t = dummy_context(); let t = dummy_context().await;
let src_ext = t.dir.path().join("external"); let src_ext = t.dir.path().join("external");
fs::write(&src_ext, b"boo").unwrap(); fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap(); let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/external"); assert_eq!(blob.as_name(), "$BLOBDIR/external");
let data = fs::read(blob.to_abs_path()).unwrap(); let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo"); assert_eq!(data, b"boo");
let src_int = t.ctx.get_blobdir().join("internal"); let src_int = t.ctx.get_blobdir().join("internal");
fs::write(&src_int, b"boo").unwrap(); fs::write(&src_int, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_int).unwrap(); let blob = BlobObject::new_from_path(&t.ctx, &src_int).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/internal"); assert_eq!(blob.as_name(), "$BLOBDIR/internal");
let data = fs::read(blob.to_abs_path()).unwrap(); let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo"); assert_eq!(data, b"boo");
} }
#[test] #[async_std::test]
fn test_create_from_name_long() { async fn test_create_from_name_long() {
let t = dummy_context(); let t = dummy_context().await;
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html"); let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
fs::write(&src_ext, b"boo").unwrap(); fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap(); let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
assert_eq!( assert_eq!(
blob.as_name(), blob.as_name(),
"$BLOBDIR/autocrypt-setup-message-4137848473.html" "$BLOBDIR/autocrypt-setup-message-4137848473.html"

File diff suppressed because it is too large Load Diff

View File

@@ -86,7 +86,7 @@ impl Chatlist {
/// are returned. /// are returned.
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID /// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
/// are returned. /// are returned.
pub fn try_load( pub async fn try_load(
context: &Context, context: &Context,
listflags: usize, listflags: usize,
query: Option<&str>, query: Option<&str>,
@@ -99,7 +99,7 @@ impl Chatlist {
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some // Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
// messages get deleted to avoid reloading the same chatlist. // messages get deleted to avoid reloading the same chatlist.
if let Err(err) = delete_device_expired_messages(context) { if let Err(err) = delete_device_expired_messages(context).await {
warn!(context, "Failed to hide expired messages: {}", err); warn!(context, "Failed to hide expired messages: {}", err);
} }
@@ -118,6 +118,7 @@ impl Chatlist {
let skip_id = if flag_for_forwarding { let skip_id = if flag_for_forwarding {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE) chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.await
.unwrap_or_default() .unwrap_or_default()
.0 .0
} else { } else {
@@ -156,17 +157,19 @@ impl Chatlist {
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=?2)
GROUP BY c.id GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", 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], paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
process_row, process_row,
process_rows, process_rows,
)? ).await?
} else if flag_archived_only { } else if flag_archived_only {
// show archived chats // show archived chats
// (this includes the archived device-chat; we could skip it, // (this includes the archived device-chat; we could skip it,
// however, then the number of archived chats do not match, which might be even more irritating. // however, then the number of archived chats do not match, which might be even more irritating.
// and adapting the number requires larger refactorings and seems not to be worth the effort) // and adapting the number requires larger refactorings and seems not to be worth the effort)
context.sql.query_map( context
"SELECT c.id, m.id .sql
.query_map(
"SELECT c.id, m.id
FROM chats c FROM chats c
LEFT JOIN msgs m LEFT JOIN msgs m
ON c.id=m.chat_id ON c.id=m.chat_id
@@ -180,23 +183,26 @@ impl Chatlist {
AND c.archived=1 AND c.archived=1
GROUP BY c.id GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft], paramsv![MessageState::OutDraft],
process_row, process_row,
process_rows, process_rows,
)? )
.await?
} else if let Some(query) = query { } else if let Some(query) = query {
let query = query.trim().to_string(); let query = query.trim().to_string();
ensure!(!query.is_empty(), "missing query"); ensure!(!query.is_empty(), "missing query");
// allow searching over special names that may change at any time // allow searching over special names that may change at any time
// when the ui calls set_stock_translation() // when the ui calls set_stock_translation()
if let Err(err) = update_special_chat_names(context) { if let Err(err) = update_special_chat_names(context).await {
warn!(context, "cannot update special chat names: {:?}", err) warn!(context, "cannot update special chat names: {:?}", err)
} }
let str_like_cmd = format!("%{}%", query); let str_like_cmd = format!("%{}%", query);
context.sql.query_map( context
"SELECT c.id, m.id .sql
.query_map(
"SELECT c.id, m.id
FROM chats c FROM chats c
LEFT JOIN msgs m LEFT JOIN msgs m
ON c.id=m.chat_id ON c.id=m.chat_id
@@ -210,14 +216,16 @@ impl Chatlist {
AND c.name LIKE ?3 AND c.name LIKE ?3
GROUP BY c.id GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, skip_id, str_like_cmd], paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
process_row, process_row,
process_rows, process_rows,
)? )
.await?
} else { } else {
// show normal chatlist // show normal chatlist
let sort_id_up = if flag_for_forwarding { let sort_id_up = if flag_for_forwarding {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF) chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.await
.unwrap_or_default() .unwrap_or_default()
.0 .0
} else { } else {
@@ -238,12 +246,13 @@ impl Chatlist {
AND NOT c.archived=?3 AND NOT c.archived=?3
GROUP BY c.id GROUP BY c.id
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned], paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
process_row, process_row,
process_rows, process_rows,
)?; ).await?;
if !flag_no_specials { if !flag_no_specials {
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) { if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
{
if !flag_for_forwarding { if !flag_for_forwarding {
ids.insert( ids.insert(
0, 0,
@@ -256,7 +265,7 @@ impl Chatlist {
ids ids
}; };
if add_archived_link_item && dc_get_archived_cnt(context) > 0 { if add_archived_link_item && dc_get_archived_cnt(context).await > 0 {
if ids.is_empty() && flag_add_alldone_hint { if ids.is_empty() && flag_add_alldone_hint {
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0))); ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
} }
@@ -310,7 +319,7 @@ impl Chatlist {
/// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable. /// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable.
/// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()). /// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()).
// 0 if not applicable. // 0 if not applicable.
pub fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot { pub async fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot {
// The summary is created by the chat, not by the last message. // The summary is created by the chat, not by the last message.
// This is because we may want to display drafts here or stuff as // This is because we may want to display drafts here or stuff as
// "is typing". // "is typing".
@@ -328,7 +337,7 @@ impl Chatlist {
let chat_loaded: Chat; let chat_loaded: Chat;
let chat = if let Some(chat) = chat { let chat = if let Some(chat) = chat {
chat chat
} else if let Ok(chat) = Chat::load_from_db(context, *chat_id) { } else if let Ok(chat) = Chat::load_from_db(context, *chat_id).await {
chat_loaded = chat; chat_loaded = chat;
&chat_loaded &chat_loaded
} else { } else {
@@ -337,11 +346,11 @@ impl Chatlist {
let mut lastcontact = None; let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id) { let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id).await {
if lastmsg.from_id != DC_CONTACT_ID_SELF if lastmsg.from_id != DC_CONTACT_ID_SELF
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup) && (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{ {
lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok(); lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok();
} }
Some(lastmsg) Some(lastmsg)
@@ -353,9 +362,15 @@ impl Chatlist {
ret.text2 = None; ret.text2 = None;
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED } else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
{ {
ret.text2 = Some(context.stock_str(StockMessage::NoMessages).to_string()); ret.text2 = Some(
context
.stock_str(StockMessage::NoMessages)
.await
.to_string(),
);
} else { } else {
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context); ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context)
.await;
} }
ret ret
@@ -367,34 +382,38 @@ impl Chatlist {
} }
/// Returns the number of archived chats /// Returns the number of archived chats
pub fn dc_get_archived_cnt(context: &Context) -> u32 { pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
context context
.sql .sql
.query_get_value( .query_get_value(
context, context,
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;", "SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
params![], paramsv![],
) )
.await
.unwrap_or_default() .unwrap_or_default()
} }
fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> { async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
// We have an index over the state-column, this should be // We have an index over the state-column, this should be
// sufficient as there are typically only few fresh messages. // sufficient as there are typically only few fresh messages.
context.sql.query_get_value( context
context, .sql
concat!( .query_get_value(
"SELECT m.id", context,
" FROM msgs m", concat!(
" LEFT JOIN chats c", "SELECT m.id",
" ON c.id=m.chat_id", " FROM msgs m",
" WHERE m.state=10", " LEFT JOIN chats c",
" AND m.hidden=0", " ON c.id=m.chat_id",
" AND c.blocked=2", " WHERE m.state=10",
" ORDER BY m.timestamp DESC, m.id DESC;" " AND m.hidden=0",
), " AND c.blocked=2",
params![], " ORDER BY m.timestamp DESC, m.id DESC;"
) ),
paramsv![],
)
.await
} }
#[cfg(test)] #[cfg(test)]
@@ -403,15 +422,21 @@ mod tests {
use crate::test_utils::*; use crate::test_utils::*;
#[test] #[async_std::test]
fn test_try_load() { async fn test_try_load() {
let t = dummy_context(); let t = dummy_context().await;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap(); let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat").unwrap(); .await
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat").unwrap(); .unwrap();
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat")
.await
.unwrap();
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat")
.await
.unwrap();
// check that the chatlist starts with the most recent message // check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3); assert_eq!(chats.len(), 3);
assert_eq!(chats.get_chat_id(0), chat_id3); assert_eq!(chats.get_chat_id(0), chat_id3);
assert_eq!(chats.get_chat_id(1), chat_id2); assert_eq!(chats.get_chat_id(1), chat_id2);
@@ -420,77 +445,102 @@ mod tests {
// drafts are sorted to the top // drafts are sorted to the top
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string())); msg.set_text(Some("hello".to_string()));
chat_id2.set_draft(&t.ctx, Some(&mut msg)); chat_id2.set_draft(&t.ctx, Some(&mut msg)).await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.get_chat_id(0), chat_id2); assert_eq!(chats.get_chat_id(0), chat_id2);
// check chatlist query and archive functionality // check chatlist query and archive functionality
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None).unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None)
.await
.unwrap();
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap(); let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 0); assert_eq!(chats.len(), 0);
chat_id1 chat_id1
.set_visibility(&t.ctx, ChatVisibility::Archived) .set_visibility(&t.ctx, ChatVisibility::Archived)
.await
.ok(); .ok();
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap(); let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
} }
#[test] #[async_std::test]
fn test_sort_self_talk_up_on_forward() { async fn test_sort_self_talk_up_on_forward() {
let t = dummy_context(); let t = dummy_context().await;
t.ctx.update_device_chats().unwrap(); t.ctx.update_device_chats().await.unwrap();
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap(); create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert!(chats.len() == 3); assert!(chats.len() == 3);
assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0)) assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
.await
.unwrap() .unwrap()
.is_self_talk()); .is_self_talk());
let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None).unwrap(); let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None)
.await
.unwrap();
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
assert!(Chat::load_from_db(&t.ctx, chats.get_chat_id(0)) assert!(Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
.await
.unwrap() .unwrap()
.is_self_talk()); .is_self_talk());
} }
#[test] #[async_std::test]
fn test_search_special_chat_names() { async fn test_search_special_chat_names() {
let t = dummy_context(); let t = dummy_context().await;
t.ctx.update_device_chats().unwrap(); t.ctx.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
.await
.unwrap();
assert_eq!(chats.len(), 0); assert_eq!(chats.len(), 0);
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None)
.await
.unwrap();
assert_eq!(chats.len(), 0); assert_eq!(chats.len(), 0);
t.ctx t.ctx
.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string()) .set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
.await
.unwrap(); .unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap();
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
t.ctx t.ctx
.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string()) .set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None)
.await
.unwrap(); .unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
} }
#[test] #[async_std::test]
fn test_get_summary_unwrap() { async fn test_get_summary_unwrap() {
let t = dummy_context(); let t = dummy_context().await;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap(); let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foo:\nbar \r\n test".to_string())); msg.set_text(Some("foo:\nbar \r\n test".to_string()));
chat_id1.set_draft(&t.ctx, Some(&mut msg)); chat_id1.set_draft(&t.ctx, Some(&mut msg)).await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let summary = chats.get_summary(&t.ctx, 0, None); let summary = chats.get_summary(&t.ctx, 0, None).await;
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
} }
} }

View File

@@ -9,11 +9,9 @@ use crate::constants::DC_VERSION_STR;
use crate::context::Context; use crate::context::Context;
use crate::dc_tools::*; use crate::dc_tools::*;
use crate::events::Event; use crate::events::Event;
use crate::job::*;
use crate::message::MsgId; use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE; use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::stock::StockMessage; use crate::stock::StockMessage;
use rusqlite::NO_PARAMS;
/// The available configuration keys. /// The available configuration keys.
#[derive( #[derive(
@@ -120,16 +118,16 @@ pub enum Config {
impl Context { impl Context {
/// Get a configuration key. Returns `None` if no value is set, and no default value found. /// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub fn get_config(&self, key: Config) -> Option<String> { pub async fn get_config(&self, key: Config) -> Option<String> {
let value = match key { let value = match key {
Config::Selfavatar => { Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(self, key); let rel_path = self.sql.get_raw_config(self, key).await;
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned()) rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
} }
Config::SysVersion => Some((&*DC_VERSION_STR).clone()), Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)), Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()), Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(self, key), _ => self.sql.get_raw_config(self, key).await,
}; };
if value.is_some() { if value.is_some() {
@@ -138,27 +136,28 @@ impl Context {
// Default values // Default values
match key { match key {
Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).into_owned()), Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).await.into_owned()),
_ => key.get_str("default").map(|s| s.to_string()), _ => key.get_str("default").map(|s| s.to_string()),
} }
} }
pub fn get_config_int(&self, key: Config) -> i32 { pub async fn get_config_int(&self, key: Config) -> i32 {
self.get_config(key) self.get_config(key)
.await
.and_then(|s| s.parse().ok()) .and_then(|s| s.parse().ok())
.unwrap_or_default() .unwrap_or_default()
} }
pub fn get_config_bool(&self, key: Config) -> bool { pub async fn get_config_bool(&self, key: Config) -> bool {
self.get_config_int(key) != 0 self.get_config_int(key).await != 0
} }
/// Gets configured "delete_server_after" value. /// Gets configured "delete_server_after" value.
/// ///
/// `None` means never delete the message, `Some(0)` means delete /// `None` means never delete the message, `Some(0)` means delete
/// at once, `Some(x)` means delete after `x` seconds. /// at once, `Some(x)` means delete after `x` seconds.
pub fn get_config_delete_server_after(&self) -> Option<i64> { pub async fn get_config_delete_server_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteServerAfter) { match self.get_config_int(Config::DeleteServerAfter).await {
0 => None, 0 => None,
1 => Some(0), 1 => Some(0),
x => Some(x as i64), x => Some(x as i64),
@@ -169,8 +168,8 @@ impl Context {
/// ///
/// `None` means never delete the message, `Some(x)` means delete /// `None` means never delete the message, `Some(x)` means delete
/// after `x` seconds. /// after `x` seconds.
pub fn get_config_delete_device_after(&self) -> Option<i64> { pub async fn get_config_delete_device_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteDeviceAfter) { match self.get_config_int(Config::DeleteDeviceAfter).await {
0 => None, 0 => None,
x => Some(x as i64), x => Some(x as i64),
} }
@@ -178,57 +177,61 @@ impl Context {
/// Set the given config key. /// Set the given config key.
/// If `None` is passed as a value the value is cleared and set to the default if there is one. /// If `None` is passed as a value the value is cleared and set to the default if there is one.
pub fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> { pub async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
match key { match key {
Config::Selfavatar => { Config::Selfavatar => {
self.sql self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", NO_PARAMS)?; .execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
.await?;
self.sql self.sql
.set_raw_config_bool(self, "attach_selfavatar", true)?; .set_raw_config_bool(self, "attach_selfavatar", true)
.await?;
match value { match value {
Some(value) => { Some(value) => {
let blob = BlobObject::new_from_path(&self, value)?; let blob = BlobObject::new_from_path(&self, value).await?;
blob.recode_to_avatar_size(self)?; blob.recode_to_avatar_size(self)?;
self.sql.set_raw_config(self, key, Some(blob.as_name())) self.sql
.set_raw_config(self, key, Some(blob.as_name()))
.await
} }
None => self.sql.set_raw_config(self, key, None), None => self.sql.set_raw_config(self, key, None).await,
} }
} }
Config::InboxWatch => { Config::InboxWatch => {
let ret = self.sql.set_raw_config(self, key, value); let ret = self.sql.set_raw_config(self, key, value).await;
interrupt_inbox_idle(self); self.interrupt_inbox(false).await;
ret ret
} }
Config::SentboxWatch => { Config::SentboxWatch => {
let ret = self.sql.set_raw_config(self, key, value); let ret = self.sql.set_raw_config(self, key, value).await;
interrupt_sentbox_idle(self); self.interrupt_sentbox(false).await;
ret ret
} }
Config::MvboxWatch => { Config::MvboxWatch => {
let ret = self.sql.set_raw_config(self, key, value); let ret = self.sql.set_raw_config(self, key, value).await;
interrupt_mvbox_idle(self); self.interrupt_mvbox(false).await;
ret ret
} }
Config::Selfstatus => { Config::Selfstatus => {
let def = self.stock_str(StockMessage::StatusLine); let def = self.stock_str(StockMessage::StatusLine).await;
let val = if value.is_none() || value.unwrap() == def { let val = if value.is_none() || value.unwrap() == def {
None None
} else { } else {
value value
}; };
self.sql.set_raw_config(self, key, val) self.sql.set_raw_config(self, key, val).await
} }
Config::DeleteDeviceAfter => { Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(self, key, value); let ret = self.sql.set_raw_config(self, key, value).await;
// Force chatlist reload to delete old messages immediately. // Force chatlist reload to delete old messages immediately.
self.call_cb(Event::MsgsChanged { self.emit_event(Event::MsgsChanged {
msg_id: MsgId::new(0), msg_id: MsgId::new(0),
chat_id: ChatId::new(0), chat_id: ChatId::new(0),
}); });
ret ret
} }
_ => self.sql.set_raw_config(self, key, value), _ => self.sql.set_raw_config(self, key, value).await,
} }
} }
} }
@@ -276,9 +279,9 @@ mod tests {
assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX")); assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX"));
} }
#[test] #[async_std::test]
fn test_selfavatar_outside_blobdir() { async fn test_selfavatar_outside_blobdir() {
let t = dummy_context(); let t = dummy_context().await;
let avatar_src = t.dir.path().join("avatar.jpg"); let avatar_src = t.dir.path().join("avatar.jpg");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg"); let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
File::create(&avatar_src) File::create(&avatar_src)
@@ -286,13 +289,14 @@ mod tests {
.write_all(avatar_bytes) .write_all(avatar_bytes)
.unwrap(); .unwrap();
let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg"); let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists()); assert!(!avatar_blob.exists().await);
t.ctx t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap())) .set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.await
.unwrap(); .unwrap();
assert!(avatar_blob.exists()); assert!(avatar_blob.exists().await);
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64); assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
let avatar_cfg = t.ctx.get_config(Config::Selfavatar); let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string())); assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap(); let img = image::open(avatar_src).unwrap();
@@ -304,9 +308,9 @@ mod tests {
assert_eq!(img.height(), AVATAR_SIZE); assert_eq!(img.height(), AVATAR_SIZE);
} }
#[test] #[async_std::test]
fn test_selfavatar_in_blobdir() { async fn test_selfavatar_in_blobdir() {
let t = dummy_context(); let t = dummy_context().await;
let avatar_src = t.ctx.get_blobdir().join("avatar.png"); let avatar_src = t.ctx.get_blobdir().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png"); let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
File::create(&avatar_src) File::create(&avatar_src)
@@ -320,8 +324,9 @@ mod tests {
t.ctx t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap())) .set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.await
.unwrap(); .unwrap();
let avatar_cfg = t.ctx.get_config(Config::Selfavatar); let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string())); assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap(); let img = image::open(avatar_src).unwrap();
@@ -329,9 +334,9 @@ mod tests {
assert_eq!(img.height(), AVATAR_SIZE); assert_eq!(img.height(), AVATAR_SIZE);
} }
#[test] #[async_std::test]
fn test_selfavatar_copy_without_recode() { async fn test_selfavatar_copy_without_recode() {
let t = dummy_context(); let t = dummy_context().await;
let avatar_src = t.dir.path().join("avatar.png"); let avatar_src = t.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png"); let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
File::create(&avatar_src) File::create(&avatar_src)
@@ -339,30 +344,34 @@ mod tests {
.write_all(avatar_bytes) .write_all(avatar_bytes)
.unwrap(); .unwrap();
let avatar_blob = t.ctx.get_blobdir().join("avatar.png"); let avatar_blob = t.ctx.get_blobdir().join("avatar.png");
assert!(!avatar_blob.exists()); assert!(!avatar_blob.exists().await);
t.ctx t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap())) .set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.await
.unwrap(); .unwrap();
assert!(avatar_blob.exists()); assert!(avatar_blob.exists().await);
assert_eq!( assert_eq!(
std::fs::metadata(&avatar_blob).unwrap().len(), std::fs::metadata(&avatar_blob).unwrap().len(),
avatar_bytes.len() as u64 avatar_bytes.len() as u64
); );
let avatar_cfg = t.ctx.get_config(Config::Selfavatar); let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string())); assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
} }
#[test] #[async_std::test]
fn test_media_quality_config_option() { async fn test_media_quality_config_option() {
let t = dummy_context(); let t = dummy_context().await;
let media_quality = t.ctx.get_config_int(Config::MediaQuality); let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
assert_eq!(media_quality, 0); assert_eq!(media_quality, 0);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Balanced); assert_eq!(media_quality, constants::MediaQuality::Balanced);
t.ctx.set_config(Config::MediaQuality, Some("1")).unwrap(); t.ctx
.set_config(Config::MediaQuality, Some("1"))
.await
.unwrap();
let media_quality = t.ctx.get_config_int(Config::MediaQuality); let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
assert_eq!(media_quality, 1); assert_eq!(media_quality, 1);
assert_eq!(constants::MediaQuality::Worse as i32, 1); assert_eq!(constants::MediaQuality::Worse as i32, 1);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default(); let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();

View File

@@ -94,12 +94,12 @@ fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
} }
} }
pub fn moz_autoconfigure( pub async fn moz_autoconfigure(
context: &Context, context: &Context,
url: &str, url: &str,
param_in: &LoginParam, param_in: &LoginParam,
) -> Result<LoginParam, Error> { ) -> Result<LoginParam, Error> {
let xml_raw = read_url(context, url)?; let xml_raw = read_url(context, url).await?;
let res = parse_xml(&param_in.addr, &xml_raw); let res = parse_xml(&param_in.addr, &xml_raw);
if let Err(err) = &res { if let Err(err) = &res {

View File

@@ -112,7 +112,7 @@ fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
Ok(res) Ok(res)
} }
pub fn outlk_autodiscover( pub async fn outlk_autodiscover(
context: &Context, context: &Context,
url: &str, url: &str,
_param_in: &LoginParam, _param_in: &LoginParam,
@@ -120,7 +120,7 @@ pub fn outlk_autodiscover(
let mut url = url.to_string(); let mut url = url.to_string();
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */ /* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
for _i in 0..10 { for _i in 0..10 {
let xml_raw = read_url(context, &url)?; let xml_raw = read_url(context, &url).await?;
let res = parse_xml(&xml_raw); let res = parse_xml(&xml_raw);
if let Err(err) = &res { if let Err(err) = &res {
warn!(context, "{}", err); warn!(context, "{}", err);

File diff suppressed because it is too large Load Diff

View File

@@ -3,17 +3,13 @@ use crate::context::Context;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("URL request error")] #[error("URL request error")]
GetError(#[from] reqwest::Error), GetError(surf::Error),
} }
pub fn read_url(context: &Context, url: &str) -> Result<String, Error> { pub async fn read_url(context: &Context, url: &str) -> Result<String, Error> {
info!(context, "Requesting URL {}", url); info!(context, "Requesting URL {}", url);
match reqwest::blocking::Client::new() match surf::get(url).recv_string().await {
.get(url)
.send()
.and_then(|res| res.text())
{
Ok(res) => Ok(res), Ok(res) => Ok(res),
Err(err) => { Err(err) => {
info!(context, "Can\'t read URL {}", url); info!(context, "Can\'t read URL {}", url);

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,10 @@
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString; use std::ffi::OsString;
use std::path::{Path, PathBuf}; use std::ops::Deref;
use std::sync::{Arc, Condvar, Mutex, RwLock};
use async_std::path::{Path, PathBuf};
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
use crate::chat::*; use crate::chat::*;
use crate::config::Config; use crate::config::Config;
@@ -11,61 +13,58 @@ use crate::constants::*;
use crate::contact::*; use crate::contact::*;
use crate::dc_tools::duration_to_str; use crate::dc_tools::duration_to_str;
use crate::error::*; use crate::error::*;
use crate::events::Event; use crate::events::{Event, EventEmitter, Events};
use crate::imap::*; use crate::job::{self, Action};
use crate::job::*;
use crate::job_thread::JobThread;
use crate::key::{DcKey, Key, SignedPublicKey}; use crate::key::{DcKey, Key, SignedPublicKey};
use crate::login_param::LoginParam; use crate::login_param::LoginParam;
use crate::lot::Lot; use crate::lot::Lot;
use crate::message::{self, Message, MessengerMessage, MsgId}; use crate::message::{self, Message, MessengerMessage, MsgId};
use crate::param::Params; use crate::param::Params;
use crate::smtp::Smtp; use crate::scheduler::Scheduler;
use crate::sql::Sql; use crate::sql::Sql;
use std::time::SystemTime; use std::time::SystemTime;
/// Callback function type for [Context] #[derive(Clone, Debug)]
///
/// # Parameters
///
/// * `context` - The context object as returned by [Context::new].
/// * `event` - One of the [Event] items.
/// * `data1` - Depends on the event parameter, see [Event].
/// * `data2` - Depends on the event parameter, see [Event].
pub type ContextCallback = dyn Fn(&Context, Event) -> () + Send + Sync;
#[derive(DebugStub)]
pub struct Context { pub struct Context {
pub(crate) inner: Arc<InnerContext>,
}
impl Deref for Context {
type Target = InnerContext;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[derive(Debug)]
pub struct InnerContext {
/// Database file path /// Database file path
dbfile: PathBuf, pub(crate) dbfile: PathBuf,
/// Blob directory path /// Blob directory path
blobdir: PathBuf, pub(crate) blobdir: PathBuf,
pub sql: Sql, pub(crate) sql: Sql,
pub perform_inbox_jobs_needed: Arc<RwLock<bool>>, pub(crate) os_name: Option<String>,
pub probe_imap_network: Arc<RwLock<bool>>, pub(crate) bob: RwLock<BobStatus>,
pub inbox_thread: Arc<RwLock<JobThread>>, pub(crate) last_smeared_timestamp: RwLock<i64>,
pub sentbox_thread: Arc<RwLock<JobThread>>, pub(crate) running_state: RwLock<RunningState>,
pub mvbox_thread: Arc<RwLock<JobThread>>,
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 last_smeared_timestamp: RwLock<i64>,
pub running_state: Arc<RwLock<RunningState>>,
/// Mutex to avoid generating the key for the user more than once. /// Mutex to avoid generating the key for the user more than once.
pub generating_key_mutex: Mutex<()>, pub(crate) generating_key_mutex: Mutex<()>,
pub translated_stockstrings: RwLock<HashMap<usize, String>>, /// Mutex to enforce only a single running oauth2 is running.
pub(crate) oauth2_mutex: Mutex<()>,
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
pub(crate) events: Events,
pub(crate) scheduler: RwLock<Scheduler>,
creation_time: SystemTime, creation_time: SystemTime,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug)]
pub struct RunningState { pub struct RunningState {
pub ongoing_running: bool, pub ongoing_running: bool,
shall_stop_ongoing: bool, shall_stop_ongoing: bool,
cancel_sender: Option<Sender<()>>,
} }
/// Return some info about deltachat-core /// Return some info about deltachat-core
@@ -85,73 +84,95 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
impl Context { impl Context {
/// Creates new context. /// Creates new context.
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> { pub async fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
pretty_env_logger::try_init_timed().ok(); // pretty_env_logger::try_init_timed().ok();
let mut blob_fname = OsString::new(); let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default()); blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs"); blob_fname.push("-blobs");
let blobdir = dbfile.with_file_name(blob_fname); let blobdir = dbfile.with_file_name(blob_fname);
if !blobdir.exists() { if !blobdir.exists().await {
std::fs::create_dir_all(&blobdir)?; async_std::fs::create_dir_all(&blobdir).await?;
} }
Context::with_blobdir(cb, os_name, dbfile, blobdir) Context::with_blobdir(os_name, dbfile, blobdir).await
} }
pub fn with_blobdir( pub async fn with_blobdir(
cb: Box<ContextCallback>,
os_name: String, os_name: String,
dbfile: PathBuf, dbfile: PathBuf,
blobdir: PathBuf, blobdir: PathBuf,
) -> Result<Context> { ) -> Result<Context> {
ensure!( ensure!(
blobdir.is_dir(), blobdir.is_dir().await,
"Blobdir does not exist: {}", "Blobdir does not exist: {}",
blobdir.display() blobdir.display()
); );
let ctx = Context {
let inner = InnerContext {
blobdir, blobdir,
dbfile, dbfile,
cb,
os_name: Some(os_name), os_name: Some(os_name),
running_state: Arc::new(RwLock::new(Default::default())), running_state: RwLock::new(Default::default()),
sql: Sql::new(), sql: Sql::new(),
smtp: Arc::new(Mutex::new(Smtp::new())), bob: RwLock::new(Default::default()),
smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
oauth2_critical: Arc::new(Mutex::new(())),
bob: Arc::new(RwLock::new(Default::default())),
last_smeared_timestamp: RwLock::new(0), last_smeared_timestamp: RwLock::new(0),
cmdline_sel_chat_id: Arc::new(RwLock::new(ChatId::new(0))),
inbox_thread: Arc::new(RwLock::new(JobThread::new(
"INBOX",
"configured_inbox_folder",
Imap::new(),
))),
sentbox_thread: Arc::new(RwLock::new(JobThread::new(
"SENTBOX",
"configured_sentbox_folder",
Imap::new(),
))),
mvbox_thread: Arc::new(RwLock::new(JobThread::new(
"MVBOX",
"configured_mvbox_folder",
Imap::new(),
))),
probe_imap_network: Arc::new(RwLock::new(false)),
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
generating_key_mutex: Mutex::new(()), generating_key_mutex: Mutex::new(()),
oauth2_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()), translated_stockstrings: RwLock::new(HashMap::new()),
events: Events::default(),
scheduler: RwLock::new(Scheduler::Stopped),
creation_time: std::time::SystemTime::now(), creation_time: std::time::SystemTime::now(),
}; };
let ctx = Context {
inner: Arc::new(inner),
};
ensure!( ensure!(
ctx.sql.open(&ctx, &ctx.dbfile, false), ctx.sql.open(&ctx, &ctx.dbfile, false).await,
"Failed opening sqlite database" "Failed opening sqlite database"
); );
Ok(ctx) Ok(ctx)
} }
/// Starts the IO scheduler.
pub async fn start_io(&self) {
info!(self, "starting IO");
if self.is_io_running().await {
info!(self, "IO is already running");
return;
}
{
let l = &mut *self.inner.scheduler.write().await;
l.start(self.clone()).await;
}
}
/// Returns if the IO scheduler is running.
pub async fn is_io_running(&self) -> bool {
self.inner.is_io_running().await
}
/// Stops the IO scheduler.
pub async fn stop_io(&self) {
info!(self, "stopping IO");
if !self.is_io_running().await {
info!(self, "IO is not running");
return;
}
self.inner.stop_io().await;
}
/// Returns a reference to the underlying SQL instance.
///
/// Warning: this is only here for testing, not part of the public API.
#[cfg(feature = "internals")]
pub fn sql(&self) -> &Sql {
&self.inner.sql
}
/// Returns database file path. /// Returns database file path.
pub fn get_dbfile(&self) -> &Path { pub fn get_dbfile(&self) -> &Path {
self.dbfile.as_path() self.dbfile.as_path()
@@ -162,49 +183,57 @@ impl Context {
self.blobdir.as_path() self.blobdir.as_path()
} }
pub fn call_cb(&self, event: Event) { /// Emits a single event.
(*self.cb)(self, event); pub fn emit_event(&self, event: Event) {
self.events.emit(event);
} }
/******************************************************************************* /// Get the next queued event.
* Ongoing process allocation/free/check pub fn get_event_emitter(&self) -> EventEmitter {
******************************************************************************/ self.events.get_emitter()
}
pub fn alloc_ongoing(&self) -> bool { // Ongoing process allocation/free/check
if self.has_ongoing() {
warn!(self, "There is already another ongoing process running.",);
false pub async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
} else { if self.has_ongoing().await {
let s_a = self.running_state.clone(); bail!("There is already another ongoing process running.");
let mut s = s_a.write().unwrap();
s.ongoing_running = true;
s.shall_stop_ongoing = false;
true
} }
let s_a = &self.running_state;
let mut s = s_a.write().await;
s.ongoing_running = true;
s.shall_stop_ongoing = false;
let (sender, receiver) = channel(1);
s.cancel_sender = Some(sender);
Ok(receiver)
} }
pub fn free_ongoing(&self) { pub async fn free_ongoing(&self) {
let s_a = self.running_state.clone(); let s_a = &self.running_state;
let mut s = s_a.write().unwrap(); let mut s = s_a.write().await;
s.ongoing_running = false; s.ongoing_running = false;
s.shall_stop_ongoing = true; s.shall_stop_ongoing = true;
s.cancel_sender.take();
} }
pub fn has_ongoing(&self) -> bool { pub async fn has_ongoing(&self) -> bool {
let s_a = self.running_state.clone(); let s_a = &self.running_state;
let s = s_a.read().unwrap(); let s = s_a.read().await;
s.ongoing_running || !s.shall_stop_ongoing s.ongoing_running || !s.shall_stop_ongoing
} }
/// Signal an ongoing process to stop. /// Signal an ongoing process to stop.
pub fn stop_ongoing(&self) { pub async fn stop_ongoing(&self) {
let s_a = self.running_state.clone(); let s_a = &self.running_state;
let mut s = s_a.write().unwrap(); let mut s = s_a.write().await;
if let Some(cancel) = s.cancel_sender.take() {
cancel.send(()).await;
}
if s.ongoing_running && !s.shall_stop_ongoing { if s.ongoing_running && !s.shall_stop_ongoing {
info!(self, "Signaling the ongoing process to stop ASAP.",); info!(self, "Signaling the ongoing process to stop ASAP.",);
@@ -214,71 +243,71 @@ impl Context {
}; };
} }
pub fn shall_stop_ongoing(&self) -> bool { pub async fn shall_stop_ongoing(&self) -> bool {
self.running_state self.running_state.read().await.shall_stop_ongoing
.clone()
.read()
.unwrap()
.shall_stop_ongoing
} }
/******************************************************************************* /*******************************************************************************
* UI chat/message related API * UI chat/message related API
******************************************************************************/ ******************************************************************************/
pub fn get_info(&self) -> BTreeMap<&'static str, String> { pub async fn get_info(&self) -> BTreeMap<&'static str, String> {
let unset = "0"; let unset = "0";
let l = LoginParam::from_database(self, ""); let l = LoginParam::from_database(self, "").await;
let l2 = LoginParam::from_database(self, "configured_"); let l2 = LoginParam::from_database(self, "configured_").await;
let displayname = self.get_config(Config::Displayname); let displayname = self.get_config(Config::Displayname).await;
let chats = get_chat_cnt(self) as usize; let chats = get_chat_cnt(self).await as usize;
let real_msgs = message::get_real_msg_cnt(self) as usize; let real_msgs = message::get_real_msg_cnt(self).await as usize;
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self) as usize; let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize;
let contacts = Contact::get_real_cnt(self) as usize; let contacts = Contact::get_real_cnt(self).await as usize;
let is_configured = self.get_config_int(Config::Configured); let is_configured = self.get_config_int(Config::Configured).await;
let dbversion = self let dbversion = self
.sql .sql
.get_raw_config_int(self, "dbversion") .get_raw_config_int(self, "dbversion")
.await
.unwrap_or_default(); .unwrap_or_default();
let journal_mode = self let journal_mode = self
.sql .sql
.query_get_value(self, "PRAGMA journal_mode;", rusqlite::NO_PARAMS) .query_get_value(self, "PRAGMA journal_mode;", paramsv![])
.await
.unwrap_or_else(|| "unknown".to_string()); .unwrap_or_else(|| "unknown".to_string());
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled); let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled); let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await;
let bcc_self = self.get_config_int(Config::BccSelf); let bcc_self = self.get_config_int(Config::BccSelf).await;
let prv_key_cnt: Option<isize> = let prv_key_cnt: Option<isize> = self
self.sql .sql
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", rusqlite::NO_PARAMS); .query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
.await;
let pub_key_cnt: Option<isize> = self.sql.query_get_value( let pub_key_cnt: Option<isize> = self
self, .sql
"SELECT COUNT(*) FROM acpeerstates;", .query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
rusqlite::NO_PARAMS, .await;
); let fingerprint_str = match SignedPublicKey::load_self(self).await {
let fingerprint_str = match SignedPublicKey::load_self(self) {
Ok(key) => Key::from(key).fingerprint(), Ok(key) => Key::from(key).fingerprint(),
Err(err) => format!("<key failure: {}>", err), Err(err) => format!("<key failure: {}>", err),
}; };
let inbox_watch = self.get_config_int(Config::InboxWatch); let inbox_watch = self.get_config_int(Config::InboxWatch).await;
let sentbox_watch = self.get_config_int(Config::SentboxWatch); let sentbox_watch = self.get_config_int(Config::SentboxWatch).await;
let mvbox_watch = self.get_config_int(Config::MvboxWatch); let mvbox_watch = self.get_config_int(Config::MvboxWatch).await;
let mvbox_move = self.get_config_int(Config::MvboxMove); let mvbox_move = self.get_config_int(Config::MvboxMove).await;
let folders_configured = self let folders_configured = self
.sql .sql
.get_raw_config_int(self, "folders_configured") .get_raw_config_int(self, "folders_configured")
.await
.unwrap_or_default(); .unwrap_or_default();
let configured_sentbox_folder = self let configured_sentbox_folder = self
.sql .sql
.get_raw_config(self, "configured_sentbox_folder") .get_raw_config(self, "configured_sentbox_folder")
.await
.unwrap_or_else(|| "<unset>".to_string()); .unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self let configured_mvbox_folder = self
.sql .sql
.get_raw_config(self, "configured_mvbox_folder") .get_raw_config(self, "configured_mvbox_folder")
.await
.unwrap_or_else(|| "<unset>".to_string()); .unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info(); let mut res = get_info();
@@ -294,6 +323,7 @@ impl Context {
res.insert( res.insert(
"selfavatar", "selfavatar",
self.get_config(Config::Selfavatar) self.get_config(Config::Selfavatar)
.await
.unwrap_or_else(|| "<unset>".to_string()), .unwrap_or_else(|| "<unset>".to_string()),
); );
res.insert("is_configured", is_configured.to_string()); res.insert("is_configured", is_configured.to_string());
@@ -325,8 +355,8 @@ impl Context {
res res
} }
pub fn get_fresh_msgs(&self) -> Vec<MsgId> { pub async fn get_fresh_msgs(&self) -> Vec<MsgId> {
let show_deaddrop = 0; let show_deaddrop: i32 = 0;
self.sql self.sql
.query_map( .query_map(
concat!( concat!(
@@ -343,7 +373,7 @@ impl Context {
" AND (c.blocked=0 OR c.blocked=?)", " AND (c.blocked=0 OR c.blocked=?)",
" ORDER BY m.timestamp DESC,m.id DESC;" " ORDER BY m.timestamp DESC,m.id DESC;"
), ),
&[10, 9, if 0 != show_deaddrop { 2 } else { 0 }], paramsv![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|row| row.get::<_, MsgId>(0), |row| row.get::<_, MsgId>(0),
|rows| { |rows| {
let mut ret = Vec::new(); let mut ret = Vec::new();
@@ -353,11 +383,12 @@ impl Context {
Ok(ret) Ok(ret)
}, },
) )
.await
.unwrap_or_default() .unwrap_or_default()
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> { pub async fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> {
let real_query = query.as_ref().trim(); let real_query = query.as_ref().trim();
if real_query.is_empty() { if real_query.is_empty() {
return Vec::new(); return Vec::new();
@@ -397,7 +428,7 @@ impl Context {
self.sql self.sql
.query_map( .query_map(
query, query,
params![chat_id, &strLikeInText, &strLikeBeg], paramsv![chat_id, strLikeInText, strLikeBeg],
|row| row.get::<_, MsgId>("id"), |row| row.get::<_, MsgId>("id"),
|rows| { |rows| {
let mut ret = Vec::new(); let mut ret = Vec::new();
@@ -407,6 +438,7 @@ impl Context {
Ok(ret) Ok(ret)
}, },
) )
.await
.unwrap_or_default() .unwrap_or_default()
} }
@@ -414,8 +446,11 @@ impl Context {
folder_name.as_ref() == "INBOX" folder_name.as_ref() == "INBOX"
} }
pub fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool { pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
let sentbox_name = self.sql.get_raw_config(self, "configured_sentbox_folder"); let sentbox_name = self
.sql
.get_raw_config(self, "configured_sentbox_folder")
.await;
if let Some(name) = sentbox_name { if let Some(name) = sentbox_name {
name == folder_name.as_ref() name == folder_name.as_ref()
} else { } else {
@@ -423,8 +458,11 @@ impl Context {
} }
} }
pub fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool { pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
let mvbox_name = self.sql.get_raw_config(self, "configured_mvbox_folder"); let mvbox_name = self
.sql
.get_raw_config(self, "configured_mvbox_folder")
.await;
if let Some(name) = mvbox_name { if let Some(name) = mvbox_name {
name == folder_name.as_ref() name == folder_name.as_ref()
@@ -433,15 +471,15 @@ impl Context {
} }
} }
pub fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) { pub async fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
if !self.get_config_bool(Config::MvboxMove) { if !self.get_config_bool(Config::MvboxMove).await {
return; return;
} }
if self.is_mvbox(folder) { if self.is_mvbox(folder).await {
return; return;
} }
if let Ok(msg) = Message::load_from_db(self, msg_id) { if let Ok(msg) = Message::load_from_db(self, msg_id).await {
if msg.is_setupmessage() { if msg.is_setupmessage() {
// do not move setup messages; // do not move setup messages;
// there may be a non-delta device that wants to handle it // there may be a non-delta device that wants to handle it
@@ -451,30 +489,32 @@ impl Context {
match msg.is_dc_message { match msg.is_dc_message {
MessengerMessage::No => {} MessengerMessage::No => {}
MessengerMessage::Yes | MessengerMessage::Reply => { MessengerMessage::Yes | MessengerMessage::Reply => {
job_add( job::add(
self, self,
Action::MoveMsg, job::Job::new(Action::MoveMsg, msg.id.to_u32(), Params::new(), 0),
msg.id.to_u32() as i32, )
Params::new(), .await;
0,
);
} }
} }
} }
} }
} }
impl Drop for Context { impl InnerContext {
fn drop(&mut self) { async fn is_io_running(&self) -> bool {
info!(self, "disconnecting inbox-thread",); self.scheduler.read().await.is_running()
self.inbox_thread.read().unwrap().imap.disconnect(self); }
info!(self, "disconnecting sentbox-thread",);
self.sentbox_thread.read().unwrap().imap.disconnect(self); async fn stop_io(&self) {
info!(self, "disconnecting mvbox-thread",); assert!(self.is_io_running().await, "context is already stopped");
self.mvbox_thread.read().unwrap().imap.disconnect(self); let token = {
info!(self, "disconnecting SMTP"); let lock = &*self.scheduler.read().await;
self.smtp.clone().lock().unwrap().disconnect(); lock.pre_stop().await
self.sql.close(self); };
{
let lock = &mut *self.scheduler.write().await;
lock.stop(token).await;
}
} }
} }
@@ -483,6 +523,7 @@ impl Default for RunningState {
RunningState { RunningState {
ongoing_running: false, ongoing_running: false,
shall_stop_ongoing: true, shall_stop_ongoing: true,
cancel_sender: None,
} }
} }
} }
@@ -494,28 +535,6 @@ pub(crate) struct BobStatus {
pub qr_scan: Option<Lot>, pub qr_scan: Option<Lot>,
} }
#[derive(Debug, PartialEq)]
pub(crate) enum PerformJobsNeeded {
Not,
AtOnce,
AvoidDos,
}
impl Default for PerformJobsNeeded {
fn default() -> Self {
Self::Not
}
}
#[derive(Default, Debug)]
pub struct SmtpState {
pub idle: bool,
pub suspended: bool,
pub doing_jobs: bool,
pub(crate) perform_jobs_needed: PerformJobsNeeded,
pub probe_network: bool,
}
pub fn get_version_str() -> &'static str { pub fn get_version_str() -> &'static str {
&DC_VERSION_STR &DC_VERSION_STR
} }
@@ -526,81 +545,81 @@ mod tests {
use crate::test_utils::*; use crate::test_utils::*;
#[test] #[async_std::test]
fn test_wrong_db() { async fn test_wrong_db() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite"); let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123").unwrap(); std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new(Box::new(|_, _| ()), "FakeOs".into(), dbfile); let res = Context::new("FakeOs".into(), dbfile.into()).await;
assert!(res.is_err()); assert!(res.is_err());
} }
#[test] #[async_std::test]
fn test_get_fresh_msgs() { async fn test_get_fresh_msgs() {
let t = dummy_context(); let t = dummy_context().await;
let fresh = t.ctx.get_fresh_msgs(); let fresh = t.ctx.get_fresh_msgs().await;
assert!(fresh.is_empty()) assert!(fresh.is_empty())
} }
#[test] #[async_std::test]
fn test_blobdir_exists() { async fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite"); let dbfile = tmp.path().join("db.sqlite");
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap(); Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs"); let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir()); assert!(blobdir.is_dir());
} }
#[test] #[async_std::test]
fn test_wrong_blogdir() { async fn test_wrong_blogdir() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite"); let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs"); let blobdir = tmp.path().join("db.sqlite-blobs");
std::fs::write(&blobdir, b"123").unwrap(); std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile); let res = Context::new("FakeOS".into(), dbfile.into()).await;
assert!(res.is_err()); assert!(res.is_err());
} }
#[test] #[async_std::test]
fn test_sqlite_parent_not_exists() { async fn test_sqlite_parent_not_exists() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let subdir = tmp.path().join("subdir"); let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite"); let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone(); let dbfile2 = dbfile.clone();
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap(); Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
assert!(subdir.is_dir()); assert!(subdir.is_dir());
assert!(dbfile2.is_file()); assert!(dbfile2.is_file());
} }
#[test] #[async_std::test]
fn test_with_empty_blobdir() { async fn test_with_empty_blobdir() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite"); let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new(); let blobdir = PathBuf::new();
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir); let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await;
assert!(res.is_err()); assert!(res.is_err());
} }
#[test] #[async_std::test]
fn test_with_blobdir_not_exists() { async fn test_with_blobdir_not_exists() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite"); let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs"); 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.into(), blobdir.into()).await;
assert!(res.is_err()); assert!(res.is_err());
} }
#[test] #[async_std::test]
fn no_crashes_on_context_deref() { async fn no_crashes_on_context_deref() {
let t = dummy_context(); let t = dummy_context().await;
std::mem::drop(t.ctx); std::mem::drop(t.ctx);
} }
#[test] #[async_std::test]
fn test_get_info() { async fn test_get_info() {
let t = dummy_context(); let t = dummy_context().await;
let info = t.ctx.get_info(); let info = t.ctx.get_info().await;
assert!(info.get("database_dir").is_some()); assert!(info.get("database_dir").is_some());
} }

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,12 @@
use core::cmp::{max, min}; use core::cmp::{max, min};
use std::borrow::Cow; use std::borrow::Cow;
use std::path::{Path, PathBuf}; use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use std::{fmt, fs};
use async_std::path::{Path, PathBuf};
use async_std::{fs, io};
use chrono::{Local, TimeZone}; use chrono::{Local, TimeZone};
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
@@ -107,9 +108,9 @@ const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
// returns the currently smeared timestamp, // returns the currently smeared timestamp,
// may be used to check if call to dc_create_smeared_timestamp() is needed or not. // may be used to check if call to dc_create_smeared_timestamp() is needed or not.
// the returned timestamp MUST NOT be used to be sent out or saved in the database! // the returned timestamp MUST NOT be used to be sent out or saved in the database!
pub(crate) fn dc_smeared_time(context: &Context) -> i64 { pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
let mut now = time(); let mut now = time();
let ts = *context.last_smeared_timestamp.read().unwrap(); let ts = *context.last_smeared_timestamp.read().await;
if ts >= now { if ts >= now {
now = ts + 1; now = ts + 1;
} }
@@ -118,11 +119,11 @@ pub(crate) fn dc_smeared_time(context: &Context) -> i64 {
} }
// returns a timestamp that is guaranteed to be unique. // returns a timestamp that is guaranteed to be unique.
pub(crate) fn dc_create_smeared_timestamp(context: &Context) -> i64 { pub(crate) async fn dc_create_smeared_timestamp(context: &Context) -> i64 {
let now = time(); let now = time();
let mut ret = now; let mut ret = now;
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().unwrap(); let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await;
if ret <= *last_smeared_timestamp { if ret <= *last_smeared_timestamp {
ret = *last_smeared_timestamp + 1; ret = *last_smeared_timestamp + 1;
if ret - now > MAX_SECONDS_TO_LEND_FROM_FUTURE { if ret - now > MAX_SECONDS_TO_LEND_FROM_FUTURE {
@@ -137,12 +138,12 @@ pub(crate) fn dc_create_smeared_timestamp(context: &Context) -> i64 {
// creates `count` timestamps that are guaranteed to be unique. // creates `count` timestamps that are guaranteed to be unique.
// the frist created timestamps is returned directly, // the frist created timestamps is returned directly,
// get the other timestamps just by adding 1..count-1 // get the other timestamps just by adding 1..count-1
pub(crate) fn dc_create_smeared_timestamps(context: &Context, count: usize) -> i64 { pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize) -> i64 {
let now = time(); let now = time();
let count = count as i64; let count = count as i64;
let mut start = now + min(count, MAX_SECONDS_TO_LEND_FROM_FUTURE) - count; let mut start = now + min(count, MAX_SECONDS_TO_LEND_FROM_FUTURE) - count;
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().unwrap(); let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await;
start = max(*last_smeared_timestamp + 1, start); start = max(*last_smeared_timestamp + 1, start);
*last_smeared_timestamp = start + count - 1; *last_smeared_timestamp = start + count - 1;
@@ -248,11 +249,8 @@ pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
/// ///
/// If `path` starts with "$BLOBDIR", replaces it with the blobdir path. /// If `path` starts with "$BLOBDIR", replaces it with the blobdir path.
/// Otherwise, returns path as is. /// Otherwise, returns path as is.
pub(crate) fn dc_get_abs_path<P: AsRef<std::path::Path>>( pub(crate) fn dc_get_abs_path<P: AsRef<Path>>(context: &Context, path: P) -> PathBuf {
context: &Context, let p: &Path = path.as_ref();
path: P,
) -> std::path::PathBuf {
let p: &std::path::Path = path.as_ref();
if let Ok(p) = p.strip_prefix("$BLOBDIR") { if let Ok(p) = p.strip_prefix("$BLOBDIR") {
context.get_blobdir().join(p) context.get_blobdir().join(p)
} else { } else {
@@ -260,20 +258,20 @@ pub(crate) fn dc_get_abs_path<P: AsRef<std::path::Path>>(
} }
} }
pub(crate) fn dc_get_filebytes(context: &Context, path: impl AsRef<std::path::Path>) -> u64 { pub(crate) async fn dc_get_filebytes(context: &Context, path: impl AsRef<Path>) -> u64 {
let path_abs = dc_get_abs_path(context, &path); let path_abs = dc_get_abs_path(context, &path);
match fs::metadata(&path_abs) { match fs::metadata(&path_abs).await {
Ok(meta) => meta.len() as u64, Ok(meta) => meta.len() as u64,
Err(_err) => 0, Err(_err) => 0,
} }
} }
pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path>) -> bool { pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) -> bool {
let path_abs = dc_get_abs_path(context, &path); let path_abs = dc_get_abs_path(context, &path);
if !path_abs.exists() { if !path_abs.exists().await {
return false; return false;
} }
if !path_abs.is_file() { if !path_abs.is_file().await {
warn!( warn!(
context, context,
"refusing to delete non-file \"{}\".", "refusing to delete non-file \"{}\".",
@@ -283,9 +281,9 @@ pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path
} }
let dpath = format!("{}", path.as_ref().to_string_lossy()); let dpath = format!("{}", path.as_ref().to_string_lossy());
match fs::remove_file(path_abs) { match fs::remove_file(path_abs).await {
Ok(_) => { Ok(_) => {
context.call_cb(Event::DeletedBlobFile(dpath)); context.emit_event(Event::DeletedBlobFile(dpath));
true true
} }
Err(err) => { Err(err) => {
@@ -295,13 +293,13 @@ pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path
} }
} }
pub(crate) fn dc_copy_file( pub(crate) async fn dc_copy_file(
context: &Context, context: &Context,
src_path: impl AsRef<std::path::Path>, src_path: impl AsRef<Path>,
dest_path: impl AsRef<std::path::Path>, dest_path: impl AsRef<Path>,
) -> bool { ) -> bool {
let src_abs = dc_get_abs_path(context, &src_path); let src_abs = dc_get_abs_path(context, &src_path);
let mut src_file = match fs::File::open(&src_abs) { let mut src_file = match fs::File::open(&src_abs).await {
Ok(file) => file, Ok(file) => file,
Err(err) => { Err(err) => {
warn!( warn!(
@@ -319,6 +317,7 @@ pub(crate) fn dc_copy_file(
.create_new(true) .create_new(true)
.write(true) .write(true)
.open(&dest_abs) .open(&dest_abs)
.await
{ {
Ok(file) => file, Ok(file) => file,
Err(err) => { Err(err) => {
@@ -332,7 +331,7 @@ pub(crate) fn dc_copy_file(
} }
}; };
match std::io::copy(&mut src_file, &mut dest_file) { match io::copy(&mut src_file, &mut dest_file).await {
Ok(_) => true, Ok(_) => true,
Err(err) => { Err(err) => {
error!( error!(
@@ -344,20 +343,20 @@ pub(crate) fn dc_copy_file(
); );
{ {
// Attempt to remove the failed file, swallow errors resulting from that. // Attempt to remove the failed file, swallow errors resulting from that.
fs::remove_file(dest_abs).ok(); fs::remove_file(dest_abs).await.ok();
} }
false false
} }
} }
} }
pub(crate) fn dc_create_folder( pub(crate) async fn dc_create_folder(
context: &Context, context: &Context,
path: impl AsRef<std::path::Path>, path: impl AsRef<Path>,
) -> Result<(), std::io::Error> { ) -> Result<(), io::Error> {
let path_abs = dc_get_abs_path(context, &path); let path_abs = dc_get_abs_path(context, &path);
if !path_abs.exists() { if !path_abs.exists().await {
match fs::create_dir_all(path_abs) { match fs::create_dir_all(path_abs).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
warn!( warn!(
@@ -375,13 +374,13 @@ pub(crate) fn dc_create_folder(
} }
/// Write a the given content to provied file path. /// Write a the given content to provied file path.
pub(crate) fn dc_write_file( pub(crate) async fn dc_write_file(
context: &Context, context: &Context,
path: impl AsRef<Path>, path: impl AsRef<Path>,
buf: &[u8], buf: &[u8],
) -> Result<(), std::io::Error> { ) -> Result<(), io::Error> {
let path_abs = dc_get_abs_path(context, &path); let path_abs = dc_get_abs_path(context, &path);
fs::write(&path_abs, buf).map_err(|err| { fs::write(&path_abs, buf).await.map_err(|err| {
warn!( warn!(
context, context,
"Cannot write {} bytes to \"{}\": {}", "Cannot write {} bytes to \"{}\": {}",
@@ -393,13 +392,10 @@ pub(crate) fn dc_write_file(
}) })
} }
pub fn dc_read_file<P: AsRef<std::path::Path>>( pub async fn dc_read_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<Vec<u8>, Error> {
context: &Context,
path: P,
) -> Result<Vec<u8>, Error> {
let path_abs = dc_get_abs_path(context, &path); let path_abs = dc_get_abs_path(context, &path);
match fs::read(&path_abs) { match fs::read(&path_abs).await {
Ok(bytes) => Ok(bytes), Ok(bytes) => Ok(bytes),
Err(err) => { Err(err) => {
warn!( warn!(
@@ -413,13 +409,31 @@ pub fn dc_read_file<P: AsRef<std::path::Path>>(
} }
} }
pub fn dc_open_file<P: AsRef<std::path::Path>>( pub async fn dc_open_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<fs::File, Error> {
let path_abs = dc_get_abs_path(context, &path);
match fs::File::open(&path_abs).await {
Ok(bytes) => Ok(bytes),
Err(err) => {
warn!(
context,
"Cannot read \"{}\" or file is empty: {}",
path.as_ref().display(),
err
);
Err(err.into())
}
}
}
pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
context: &Context, context: &Context,
path: P, path: P,
) -> Result<std::fs::File, Error> { ) -> Result<std::fs::File, Error> {
let path_abs = dc_get_abs_path(context, &path); let p: PathBuf = path.as_ref().into();
let path_abs = dc_get_abs_path(context, p);
match fs::File::open(&path_abs) { match std::fs::File::open(&path_abs) {
Ok(bytes) => Ok(bytes), Ok(bytes) => Ok(bytes),
Err(err) => { Err(err) => {
warn!( warn!(
@@ -433,7 +447,7 @@ pub fn dc_open_file<P: AsRef<std::path::Path>>(
} }
} }
pub(crate) fn dc_get_next_backup_path( pub(crate) async fn dc_get_next_backup_path(
folder: impl AsRef<Path>, folder: impl AsRef<Path>,
backup_time: i64, backup_time: i64,
) -> Result<PathBuf, Error> { ) -> Result<PathBuf, Error> {
@@ -446,7 +460,7 @@ pub(crate) fn dc_get_next_backup_path(
for i in 0..64 { for i in 0..64 {
let mut path = folder.clone(); let mut path = folder.clone();
path.push(format!("{}-{}.bak", stem, i)); path.push(format!("{}-{}.bak", stem, i));
if !path.exists() { if !path.exists().await {
return Ok(path); return Ok(path);
} }
} }
@@ -760,31 +774,35 @@ mod tests {
} }
} }
#[test] #[async_std::test]
fn test_file_handling() { async fn test_file_handling() {
let t = dummy_context(); let t = dummy_context().await;
let context = &t.ctx; let context = &t.ctx;
let dc_file_exist = |ctx: &Context, fname: &str| { macro_rules! dc_file_exist {
ctx.get_blobdir() ($ctx:expr, $fname:expr) => {
.join(Path::new(fname).file_name().unwrap()) $ctx.get_blobdir()
.exists() .join(Path::new($fname).file_name().unwrap())
}; .exists()
};
assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje"));
if dc_file_exist(context, "$BLOBDIR/foobar")
|| dc_file_exist(context, "$BLOBDIR/dada")
|| dc_file_exist(context, "$BLOBDIR/foobar.dadada")
|| dc_file_exist(context, "$BLOBDIR/foobar-folder")
{
dc_delete_file(context, "$BLOBDIR/foobar");
dc_delete_file(context, "$BLOBDIR/dada");
dc_delete_file(context, "$BLOBDIR/foobar.dadada");
dc_delete_file(context, "$BLOBDIR/foobar-folder");
} }
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content").is_ok());
assert!(dc_file_exist(context, "$BLOBDIR/foobar",)); assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje").await);
assert!(!dc_file_exist(context, "$BLOBDIR/foobarx")); if dc_file_exist!(context, "$BLOBDIR/foobar").await
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar"), 7); || dc_file_exist!(context, "$BLOBDIR/dada").await
|| dc_file_exist!(context, "$BLOBDIR/foobar.dadada").await
|| dc_file_exist!(context, "$BLOBDIR/foobar-folder").await
{
dc_delete_file(context, "$BLOBDIR/foobar").await;
dc_delete_file(context, "$BLOBDIR/dada").await;
dc_delete_file(context, "$BLOBDIR/foobar.dadada").await;
dc_delete_file(context, "$BLOBDIR/foobar-folder").await;
}
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content")
.await
.is_ok());
assert!(dc_file_exist!(context, "$BLOBDIR/foobar").await);
assert!(!dc_file_exist!(context, "$BLOBDIR/foobarx").await);
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar").await, 7);
let abs_path = context let abs_path = context
.get_blobdir() .get_blobdir()
@@ -792,31 +810,33 @@ mod tests {
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
assert!(dc_file_exist(context, &abs_path)); assert!(dc_file_exist!(context, &abs_path).await);
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",)); assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
// attempting to copy a second time should fail // attempting to copy a second time should fail
assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",)); assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada",), 7); assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada").await, 7);
let buf = dc_read_file(context, "$BLOBDIR/dada").unwrap(); let buf = dc_read_file(context, "$BLOBDIR/dada").await.unwrap();
assert_eq!(buf.len(), 7); assert_eq!(buf.len(), 7);
assert_eq!(&buf, b"content"); assert_eq!(&buf, b"content");
assert!(dc_delete_file(context, "$BLOBDIR/foobar")); assert!(dc_delete_file(context, "$BLOBDIR/foobar").await);
assert!(dc_delete_file(context, "$BLOBDIR/dada")); assert!(dc_delete_file(context, "$BLOBDIR/dada").await);
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder").is_ok()); assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder")
assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",)); .await
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder")); .is_ok());
assert!(dc_file_exist!(context, "$BLOBDIR/foobar-folder").await);
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder").await);
let fn0 = "$BLOBDIR/data.data"; let fn0 = "$BLOBDIR/data.data";
assert!(dc_write_file(context, &fn0, b"content").is_ok()); assert!(dc_write_file(context, &fn0, b"content").await.is_ok());
assert!(dc_delete_file(context, &fn0)); assert!(dc_delete_file(context, &fn0).await);
assert!(!dc_file_exist(context, &fn0)); assert!(!dc_file_exist!(context, &fn0).await);
} }
#[test] #[test]
@@ -833,15 +853,15 @@ mod tests {
assert!(!listflags_has(listflags, DC_GCL_ADD_SELF)); assert!(!listflags_has(listflags, DC_GCL_ADD_SELF));
} }
#[test] #[async_std::test]
fn test_create_smeared_timestamp() { async fn test_create_smeared_timestamp() {
let t = dummy_context(); let t = dummy_context().await;
assert_ne!( assert_ne!(
dc_create_smeared_timestamp(&t.ctx), dc_create_smeared_timestamp(&t.ctx).await,
dc_create_smeared_timestamp(&t.ctx) dc_create_smeared_timestamp(&t.ctx).await
); );
assert!( assert!(
dc_create_smeared_timestamp(&t.ctx) dc_create_smeared_timestamp(&t.ctx).await
>= SystemTime::now() >= SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.unwrap() .unwrap()
@@ -849,17 +869,17 @@ mod tests {
); );
} }
#[test] #[async_std::test]
fn test_create_smeared_timestamps() { async fn test_create_smeared_timestamps() {
let t = dummy_context(); let t = dummy_context().await;
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1; let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
let start = dc_create_smeared_timestamps(&t.ctx, count as usize); let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
let next = dc_smeared_time(&t.ctx); let next = dc_smeared_time(&t.ctx).await;
assert!((start + count - 1) < next); assert!((start + count - 1) < next);
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30; let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30;
let start = dc_create_smeared_timestamps(&t.ctx, count as usize); let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
let next = dc_smeared_time(&t.ctx); let next = dc_smeared_time(&t.ctx).await;
assert!((start + count - 1) < next); assert!((start + count - 1) < next);
} }

View File

@@ -25,18 +25,18 @@ pub struct EncryptHelper {
} }
impl EncryptHelper { impl EncryptHelper {
pub fn new(context: &Context) -> Result<EncryptHelper> { pub async fn new(context: &Context) -> Result<EncryptHelper> {
let prefer_encrypt = let prefer_encrypt =
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled)) EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await)
.unwrap_or_default(); .unwrap_or_default();
let addr = match context.get_config(Config::ConfiguredAddr) { let addr = match context.get_config(Config::ConfiguredAddr).await {
None => { None => {
bail!("addr not configured!"); bail!("addr not configured!");
} }
Some(addr) => addr, Some(addr) => addr,
}; };
let public_key = SignedPublicKey::load_self(context)?; let public_key = SignedPublicKey::load_self(context).await?;
Ok(EncryptHelper { Ok(EncryptHelper {
prefer_encrypt, prefer_encrypt,
@@ -86,37 +86,37 @@ impl EncryptHelper {
} }
/// Tries to encrypt the passed in `mail`. /// Tries to encrypt the passed in `mail`.
pub fn encrypt( pub async fn encrypt(
&mut self, self,
context: &Context, context: &Context,
min_verified: PeerstateVerifiedStatus, min_verified: PeerstateVerifiedStatus,
mail_to_encrypt: lettre_email::PartBuilder, mail_to_encrypt: lettre_email::PartBuilder,
peerstates: &[(Option<Peerstate>, &str)], peerstates: Vec<(Option<Peerstate<'_>>, &str)>,
) -> Result<String> { ) -> Result<String> {
let mut keyring = Keyring::default(); let mut keyring = Keyring::default();
for (peerstate, addr) in peerstates for (peerstate, addr) in peerstates
.iter() .into_iter()
.filter_map(|(state, addr)| state.as_ref().map(|s| (s, addr))) .filter_map(|(state, addr)| state.map(|s| (s, addr)))
{ {
let key = peerstate.peek_key(min_verified).ok_or_else(|| { let key = peerstate.take_key(min_verified).ok_or_else(|| {
format_err!("proper enc-key for {} missing, cannot encrypt", addr) format_err!("proper enc-key for {} missing, cannot encrypt", addr)
})?; })?;
keyring.add_ref(key); keyring.add(key);
} }
let public_key = Key::from(self.public_key.clone()); let public_key = Key::from(self.public_key);
keyring.add_ref(&public_key); keyring.add(public_key);
let sign_key = Key::from(SignedSecretKey::load_self(context)?); let sign_key = Key::from(SignedSecretKey::load_self(context).await?);
let raw_message = mail_to_encrypt.build().as_string().into_bytes(); let raw_message = mail_to_encrypt.build().as_string().into_bytes();
let ctext = pgp::pk_encrypt(&raw_message, &keyring, Some(&sign_key))?; let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key)).await?;
Ok(ctext) Ok(ctext)
} }
} }
pub fn try_decrypt( pub async fn try_decrypt(
context: &Context, context: &Context,
mail: &ParsedMail<'_>, mail: &ParsedMail<'_>,
message_time: i64, message_time: i64,
@@ -133,54 +133,56 @@ pub fn try_decrypt(
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers); let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
if message_time > 0 { if message_time > 0 {
peerstate = Peerstate::from_addr(context, &context.sql, &from); peerstate = Peerstate::from_addr(context, &from).await;
if let Some(ref mut peerstate) = peerstate { if let Some(ref mut peerstate) = peerstate {
if let Some(ref header) = autocryptheader { if let Some(ref header) = autocryptheader {
peerstate.apply_header(&header, message_time); peerstate.apply_header(&header, message_time);
peerstate.save_to_db(&context.sql, false)?; peerstate.save_to_db(&context.sql, false).await?;
} else if message_time > peerstate.last_seen_autocrypt && !contains_report(mail) { } else if message_time > peerstate.last_seen_autocrypt && !contains_report(mail) {
peerstate.degrade_encryption(message_time); peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false)?; peerstate.save_to_db(&context.sql, false).await?;
} }
} else if let Some(ref header) = autocryptheader { } else if let Some(ref header) = autocryptheader {
let p = Peerstate::from_header(context, header, message_time); let p = Peerstate::from_header(context, header, message_time);
p.save_to_db(&context.sql, true)?; p.save_to_db(&context.sql, true).await?;
peerstate = Some(p); peerstate = Some(p);
} }
} }
/* possibly perform decryption */ /* possibly perform decryption */
let mut private_keyring = Keyring::default();
let mut public_keyring_for_validate = Keyring::default(); let mut public_keyring_for_validate = Keyring::default();
let mut out_mail = None; let mut out_mail = None;
let mut signatures = HashSet::default(); let mut signatures = HashSet::default();
let self_addr = context.get_config(Config::ConfiguredAddr); let self_addr = context.get_config(Config::ConfiguredAddr).await;
if let Some(self_addr) = self_addr { if let Some(self_addr) = self_addr {
if private_keyring.load_self_private_for_decrypting(context, self_addr, &context.sql) { if let Ok(private_keyring) =
Keyring::load_self_private_for_decrypting(context, self_addr).await
{
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 { if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
peerstate = Peerstate::from_addr(&context, &context.sql, &from); peerstate = Peerstate::from_addr(&context, &from).await;
} }
if let Some(ref peerstate) = peerstate { if let Some(peerstate) = peerstate {
if peerstate.degrade_event.is_some() { if peerstate.degrade_event.is_some() {
handle_degrade_event(context, &peerstate)?; handle_degrade_event(context, &peerstate).await?;
} }
if let Some(ref key) = peerstate.gossip_key { if let Some(key) = peerstate.gossip_key {
public_keyring_for_validate.add_ref(key); public_keyring_for_validate.add(key);
} }
if let Some(ref key) = peerstate.public_key { if let Some(key) = peerstate.public_key {
public_keyring_for_validate.add_ref(key); public_keyring_for_validate.add(key);
} }
} }
out_mail = decrypt_if_autocrypt_message( out_mail = decrypt_if_autocrypt_message(
context, context,
mail, mail,
&private_keyring, private_keyring,
&public_keyring_for_validate, public_keyring_for_validate,
&mut signatures, &mut signatures,
)?; )
.await?;
} }
} }
Ok((out_mail, signatures)) Ok((out_mail, signatures))
@@ -213,11 +215,11 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail
Ok(&mail.subparts[1]) Ok(&mail.subparts[1])
} }
fn decrypt_if_autocrypt_message<'a>( async fn decrypt_if_autocrypt_message<'a>(
context: &Context, context: &Context,
mail: &ParsedMail<'a>, mail: &ParsedMail<'a>,
private_keyring: &Keyring, private_keyring: Keyring,
public_keyring_for_validate: &Keyring, public_keyring_for_validate: Keyring,
ret_valid_signatures: &mut HashSet<String>, ret_valid_signatures: &mut HashSet<String>,
) -> Result<Option<Vec<u8>>> { ) -> Result<Option<Vec<u8>>> {
// The returned bool is true if we detected an Autocrypt-encrypted // The returned bool is true if we detected an Autocrypt-encrypted
@@ -237,20 +239,19 @@ fn decrypt_if_autocrypt_message<'a>(
info!(context, "Detected Autocrypt-mime message"); info!(context, "Detected Autocrypt-mime message");
decrypt_part( decrypt_part(
context,
encrypted_data_part, encrypted_data_part,
private_keyring, private_keyring,
public_keyring_for_validate, public_keyring_for_validate,
ret_valid_signatures, ret_valid_signatures,
) )
.await
} }
/// Returns Ok(None) if nothing encrypted was found. /// Returns Ok(None) if nothing encrypted was found.
fn decrypt_part( async fn decrypt_part(
_context: &Context,
mail: &ParsedMail<'_>, mail: &ParsedMail<'_>,
private_keyring: &Keyring, private_keyring: Keyring,
public_keyring_for_validate: &Keyring, public_keyring_for_validate: Keyring,
ret_valid_signatures: &mut HashSet<String>, ret_valid_signatures: &mut HashSet<String>,
) -> Result<Option<Vec<u8>>> { ) -> Result<Option<Vec<u8>>> {
let data = mail.get_body_raw()?; let data = mail.get_body_raw()?;
@@ -260,11 +261,12 @@ fn decrypt_part(
ensure!(ret_valid_signatures.is_empty(), "corrupt signatures"); ensure!(ret_valid_signatures.is_empty(), "corrupt signatures");
let plain = pgp::pk_decrypt( let plain = pgp::pk_decrypt(
&data, data,
&private_keyring, private_keyring,
&public_keyring_for_validate, public_keyring_for_validate,
Some(ret_valid_signatures), Some(ret_valid_signatures),
)?; )
.await?;
ensure!(!ret_valid_signatures.is_empty(), "no valid signatures"); ensure!(!ret_valid_signatures.is_empty(), "no valid signatures");
return Ok(Some(plain)); return Ok(Some(plain));
@@ -308,14 +310,17 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
/// If this succeeds you are also guaranteed that the /// If this succeeds you are also guaranteed that the
/// [Config::ConfiguredAddr] is configured, this address is returned. /// [Config::ConfiguredAddr] is configured, this address is returned.
// TODO, remove this once deltachat::key::Key no longer exists. // TODO, remove this once deltachat::key::Key no longer exists.
pub fn ensure_secret_key_exists(context: &Context) -> Result<String> { pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context.get_config(Config::ConfiguredAddr).ok_or_else(|| { let self_addr = context
format_err!(concat!( .get_config(Config::ConfiguredAddr)
"Failed to get self address, ", .await
"cannot ensure secret key if not configured." .ok_or_else(|| {
)) format_err!(concat!(
})?; "Failed to get self address, ",
SignedPublicKey::load_self(context)?; "cannot ensure secret key if not configured."
))
})?;
SignedPublicKey::load_self(context).await?;
Ok(self_addr) Ok(self_addr)
} }
@@ -328,17 +333,17 @@ mod tests {
mod ensure_secret_key_exists { mod ensure_secret_key_exists {
use super::*; use super::*;
#[test] #[async_std::test]
fn test_prexisting() { async fn test_prexisting() {
let t = dummy_context(); let t = dummy_context().await;
let test_addr = configure_alice_keypair(&t.ctx); let test_addr = configure_alice_keypair(&t.ctx).await;
assert_eq!(ensure_secret_key_exists(&t.ctx).unwrap(), test_addr); assert_eq!(ensure_secret_key_exists(&t.ctx).await.unwrap(), test_addr);
} }
#[test] #[async_std::test]
fn test_not_configured() { async fn test_not_configured() {
let t = dummy_context(); let t = dummy_context().await;
assert!(ensure_secret_key_exists(&t.ctx).is_err()); assert!(ensure_secret_key_exists(&t.ctx).await.is_err());
} }
} }

View File

@@ -2,13 +2,6 @@
pub use anyhow::{bail, ensure, format_err, Error, Result}; pub use anyhow::{bail, ensure, format_err, Error, Result};
// #[fail(display = "Invalid Message ID.")]
// InvalidMsgId,
// #[fail(display = "Watch folder not found {:?}", _0)]
// WatchFolderNotFound(String),
// #[fail(display = "Not Configured")]
// NotConfigured,
#[macro_export] #[macro_export]
macro_rules! ensure_eq { macro_rules! ensure_eq {
($left:expr, $right:expr) => ({ ($left:expr, $right:expr) => ({

View File

@@ -1,12 +1,65 @@
//! # Events specification //! # Events specification
use std::path::PathBuf; use async_std::path::PathBuf;
use async_std::sync::{channel, Receiver, Sender, TrySendError};
use strum::EnumProperty; use strum::EnumProperty;
use crate::chat::ChatId; use crate::chat::ChatId;
use crate::message::MsgId; use crate::message::MsgId;
#[derive(Debug)]
pub struct Events {
receiver: Receiver<Event>,
sender: Sender<Event>,
}
impl Default for Events {
fn default() -> Self {
let (sender, receiver) = channel(1_000);
Self { receiver, sender }
}
}
impl Events {
pub fn emit(&self, event: Event) {
match self.sender.try_send(event) {
Ok(()) => {}
Err(TrySendError::Full(event)) => {
// when we are full, we pop remove the oldest event and push on the new one
let _ = self.receiver.try_recv();
// try again
self.emit(event);
}
Err(TrySendError::Disconnected(_)) => {
unreachable!("unable to emit event, channel disconnected");
}
}
}
/// Retrieve the event emitter.
pub fn get_emitter(&self) -> EventEmitter {
EventEmitter(self.receiver.clone())
}
}
#[derive(Debug, Clone)]
pub struct EventEmitter(Receiver<Event>);
impl EventEmitter {
/// Blocking recv of an event. Return `None` if the `Sender` has been droped.
pub fn recv_sync(&self) -> Option<Event> {
async_std::task::block_on(self.recv())
}
/// Blocking async recv of an event. Return `None` if the `Sender` has been droped.
pub async fn recv(&self) -> Option<Event> {
// TODO: change once we can use async channels internally.
self.0.recv().await.ok()
}
}
impl Event { impl Event {
/// Returns the corresponding Event id. /// Returns the corresponding Event id.
pub fn as_id(&self) -> i32 { pub fn as_id(&self) -> i32 {

View File

@@ -1,20 +1,80 @@
use std::ops::{Deref, DerefMut};
use async_imap::{ use async_imap::{
error::{Error as ImapError, Result as ImapResult}, error::{Error as ImapError, Result as ImapResult},
Client as ImapClient, Client as ImapClient,
}; };
use async_native_tls::TlsStream;
use async_std::net::{self, TcpStream}; use async_std::net::{self, TcpStream};
use super::session::Session; use super::session::Session;
use crate::login_param::{dc_build_tls, CertificateChecks}; use crate::login_param::{dc_build_tls, CertificateChecks};
use super::session::SessionStream;
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum Client { pub(crate) struct Client {
Secure(ImapClient<TlsStream<TcpStream>>), is_secure: bool,
Insecure(ImapClient<TcpStream>), inner: ImapClient<Box<dyn SessionStream>>,
}
impl Deref for Client {
type Target = ImapClient<Box<dyn SessionStream>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Client {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
} }
impl Client { impl Client {
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
self,
username: U,
password: P,
) -> std::result::Result<Session, (ImapError, Self)> {
let Client { inner, is_secure } = self;
let session = inner
.login(username, password)
.await
.map_err(|(err, client)| {
(
err,
Client {
is_secure,
inner: client,
},
)
})?;
Ok(Session { inner: session })
}
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
self,
auth_type: S,
authenticator: &A,
) -> std::result::Result<Session, (ImapError, Self)> {
let Client { inner, is_secure } = self;
let session =
inner
.authenticate(auth_type, authenticator)
.await
.map_err(|(err, client)| {
(
err,
Client {
is_secure,
inner: client,
},
)
})?;
Ok(Session { inner: session })
}
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>( pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
addr: A, addr: A,
domain: S, domain: S,
@@ -22,7 +82,8 @@ impl Client {
) -> ImapResult<Self> { ) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?; let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(certificate_checks); let tls = dc_build_tls(certificate_checks);
let tls_stream = tls.connect(domain.as_ref(), stream).await?; let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(domain.as_ref(), stream).await?);
let mut client = ImapClient::new(tls_stream); let mut client = ImapClient::new(tls_stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() { if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true; client.debug = true;
@@ -33,11 +94,14 @@ impl Client {
.await .await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?; .ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Secure(client)) Ok(Client {
is_secure: true,
inner: client,
})
} }
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> { pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?; let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
let mut client = ImapClient::new(stream); let mut client = ImapClient::new(stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() { if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
@@ -48,7 +112,10 @@ impl Client {
.await .await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?; .ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Insecure(client)) Ok(Client {
is_secure: false,
inner: client,
})
} }
pub async fn secure<S: AsRef<str>>( pub async fn secure<S: AsRef<str>>(
@@ -56,49 +123,21 @@ impl Client {
domain: S, domain: S,
certificate_checks: CertificateChecks, certificate_checks: CertificateChecks,
) -> ImapResult<Client> { ) -> ImapResult<Client> {
match self { if self.is_secure {
Client::Insecure(client) => { Ok(self)
let tls = dc_build_tls(certificate_checks); } else {
let client_sec = client.secure(domain, tls).await?; let Client { mut inner, .. } = self;
let tls = dc_build_tls(certificate_checks);
inner.run_command_and_check_ok("STARTTLS", None).await?;
Ok(Client::Secure(client_sec)) let stream = inner.into_inner();
} let ssl_stream = tls.connect(domain.as_ref(), stream).await?;
// Nothing to do let boxed: Box<dyn SessionStream> = Box::new(ssl_stream);
Client::Secure(_) => Ok(self),
}
}
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>( Ok(Client {
self, is_secure: true,
auth_type: S, inner: ImapClient::new(boxed),
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,11 +1,7 @@
use super::Imap; use super::Imap;
use async_imap::extensions::idle::{Handle as ImapIdleHandle, IdleResponse}; use async_imap::extensions::idle::IdleResponse;
use async_native_tls::TlsStream;
use async_std::net::TcpStream;
use async_std::prelude::*; use async_std::prelude::*;
use async_std::task;
use std::sync::atomic::Ordering;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use crate::context::Context; use crate::context::Context;
@@ -33,255 +29,199 @@ pub enum Error {
SetupHandleError(#[from] super::Error), SetupHandleError(#[from] super::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 { impl Imap {
pub fn can_idle(&self) -> bool { pub fn can_idle(&self) -> bool {
task::block_on(async move { self.config.read().await.can_idle }) self.config.can_idle
} }
pub fn idle(&self, context: &Context, watch_folder: Option<String>) -> Result<()> { pub async fn idle(&mut self, context: &Context, watch_folder: Option<String>) -> Result<bool> {
task::block_on(async move { use futures::future::FutureExt;
if !self.can_idle() {
return Err(Error::IdleAbilityMissing); if !self.can_idle() {
return Err(Error::IdleAbilityMissing);
}
self.setup_handle_if_needed(context).await?;
self.select_folder(context, watch_folder.clone()).await?;
let session = self.session.take();
let timeout = Duration::from_secs(23 * 60);
let mut probe_network = false;
if let Some(session) = session {
let mut handle = session.idle();
if let Err(err) = handle.init().await {
return Err(Error::IdleProtocolFailed(err));
} }
self.setup_handle_if_needed(context).await?; let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
self.select_folder(context, watch_folder.clone()).await?; enum Event {
IdleResponse(IdleResponse),
let session = self.session.lock().await.take(); Interrupt(bool),
let timeout = Duration::from_secs(23 * 60);
if let Some(session) = session {
match session.idle() {
// BEWARE: If you change the Secure branch you
// typically also need to change the Insecure branch.
IdleHandle::Secure(mut handle) => {
handle.init().await?;
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
*self.interrupt.lock().await = Some(interrupt);
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
std::mem::drop(idle_wait);
info!(context, "Idle wait was skipped");
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
Ok(IdleResponse::NewData(_)) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
Ok(IdleResponse::Timeout) => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(IdleResponse::ManualInterrupt) => {
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
Ok(session) => {
*self.session.lock().await = Some(Session::Secure(session));
}
Err(err) => {
// if we cannot terminate IDLE it probably
// means that we waited long (with idle_wait)
// but the network went away/changed
self.trigger_reconnect();
return Err(Error::IdleProtocolFailed(err));
}
}
}
IdleHandle::Insecure(mut handle) => {
handle.init().await?;
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
*self.interrupt.lock().await = Some(interrupt);
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
std::mem::drop(idle_wait);
info!(context, "Idle wait was skipped");
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
Ok(IdleResponse::NewData(_)) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
Ok(IdleResponse::Timeout) => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(IdleResponse::ManualInterrupt) => {
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
Ok(session) => {
*self.session.lock().await = Some(Session::Insecure(session));
}
Err(err) => {
// if we cannot terminate IDLE it probably
// means that we waited long (with idle_wait)
// but the network went away/changed
self.trigger_reconnect();
return Err(Error::IdleProtocolFailed(err));
}
}
}
}
} }
Ok(()) if self.skip_next_idle_wait {
})
}
pub(crate) fn fake_idle(&self, context: &Context, watch_folder: Option<String>) {
// Idle using polling. This is also needed if we're not yet configured -
// in this case, we're waiting for a configure job (and an interrupt).
task::block_on(async move {
let fake_idle_start_time = SystemTime::now();
info!(context, "IMAP-fake-IDLEing...");
let interrupt = stop_token::StopSource::new();
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
let interval = async_std::stream::interval(Duration::from_secs(60));
let mut interrupt_interval = interrupt.stop_token().stop_stream(interval);
*self.interrupt.lock().await = Some(interrupt);
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
// interrupt_idle has happened before we // interrupt_idle has happened before we
// provided self.interrupt // provided self.interrupt
self.skip_next_idle_wait.store(false, Ordering::SeqCst); self.skip_next_idle_wait = false;
info!(context, "fake-idle wait was skipped"); drop(idle_wait);
} else { drop(interrupt);
// loop until we are interrupted or if we fetched something
while let Some(_) = interrupt_interval.next().await {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.connect_configured(context) {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if self.config.read().await.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break;
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
// 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.
if let Some(ref watch_folder) = watch_folder { info!(context, "Idle wait was skipped");
match self.fetch_new_messages(context, watch_folder).await { } else {
Ok(res) => { info!(context, "Idle entering wait-on-remote state");
info!(context, "fetch_new_messages returned {:?}", res); let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(
if res { self.idle_interrupt.recv().map(|probe_network| {
break; Ok(Event::Interrupt(probe_network.unwrap_or_default()))
} }),
} );
Err(err) => {
error!(context, "could not fetch from folder: {}", err); match fut.await {
self.trigger_reconnect() Ok(Event::IdleResponse(IdleResponse::NewData(_))) => {
} info!(context, "Idle has NewData");
} }
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(context, "Idle wait was interrupted");
}
Ok(Event::Interrupt(probe)) => {
probe_network = probe;
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
} }
} }
} }
self.interrupt.lock().await.take();
info!( // if we can't properly terminate the idle
context, // protocol let's break the connection.
"IMAP-fake-IDLE done after {:.4}s", let res = handle
SystemTime::now() .done()
.duration_since(fake_idle_start_time) .timeout(Duration::from_secs(15))
.unwrap_or_default() .await
.as_millis() as f64 .map_err(|err| {
/ 1000., self.trigger_reconnect();
); Error::IdleTimeout(err)
}) })?;
match res {
Ok(session) => {
self.session = Some(Session { inner: session });
}
Err(err) => {
// if we cannot terminate IDLE it probably
// means that we waited long (with idle_wait)
// but the network went away/changed
self.trigger_reconnect();
return Err(Error::IdleProtocolFailed(err));
}
}
}
Ok(probe_network)
} }
pub fn interrupt_idle(&self, context: &Context) { pub(crate) async fn fake_idle(
task::block_on(async move { &mut self,
let mut interrupt: Option<stop_token::StopSource> = self.interrupt.lock().await.take(); context: &Context,
if interrupt.is_none() { watch_folder: Option<String>,
// idle wait is not running, signal it needs to skip ) -> bool {
self.skip_next_idle_wait.store(true, Ordering::SeqCst); // Idle using polling. This is also needed if we're not yet configured -
// in this case, we're waiting for a configure job (and an interrupt).
// meanwhile idle-wait may have produced the StopSource let fake_idle_start_time = SystemTime::now();
interrupt = self.interrupt.lock().await.take(); info!(context, "IMAP-fake-IDLEing...");
// Do not poll, just wait for an interrupt when no folder is passed in.
if watch_folder.is_none() {
return self.idle_interrupt.recv().await.unwrap_or_default();
}
let mut probe_network = false;
if self.skip_next_idle_wait {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait = false;
info!(context, "fake-idle wait was skipped");
} else {
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
let mut interval = async_std::stream::interval(Duration::from_secs(60));
enum Event {
Tick,
Interrupt(bool),
} }
// let's manually drop the StopSource // loop until we are interrupted or if we fetched something
if interrupt.is_some() { probe_network =
// the imap thread provided us a stop token but might loop {
// not have entered idle_wait yet, give it some time use futures::future::FutureExt;
// for that to happen. XXX handle this without extra wait match interval
// https://github.com/deltachat/deltachat-core-rust/issues/925 .next()
std::thread::sleep(Duration::from_millis(200)); .map(|_| Event::Tick)
info!(context, "low-level: dropping stop-source to interrupt idle"); .race(self.idle_interrupt.recv().map(|probe_network| {
std::mem::drop(interrupt) Event::Interrupt(probe_network.unwrap_or_default())
} }))
}); .await
{
Event::Tick => {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.connect_configured(context).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if self.config.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break false;
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
// 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.
if let Some(ref watch_folder) = watch_folder {
match self.fetch_new_messages(context, watch_folder).await {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break false;
}
}
Err(err) => {
error!(context, "could not fetch from folder: {}", err);
self.trigger_reconnect()
}
}
}
}
Event::Interrupt(probe_network) => {
// Interrupt
break probe_network;
}
}
};
}
info!(
context,
"IMAP-fake-IDLE done after {:.4}s",
SystemTime::now()
.duration_since(fake_idle_start_time)
.unwrap_or_default()
.as_millis() as f64
/ 1000.,
);
probe_network
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -27,11 +27,11 @@ impl Imap {
/// ///
/// CLOSE is considerably faster than an EXPUNGE, see /// CLOSE is considerably faster than an EXPUNGE, see
/// https://tools.ietf.org/html/rfc3501#section-6.4.2 /// https://tools.ietf.org/html/rfc3501#section-6.4.2
async fn close_folder(&self, context: &Context) -> Result<()> { async fn close_folder(&mut self, context: &Context) -> Result<()> {
if let Some(ref folder) = self.config.read().await.selected_folder { if let Some(ref folder) = self.config.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder); info!(context, "Expunge messages in \"{}\".", folder);
if let Some(ref mut session) = &mut *self.session.lock().await { if let Some(ref mut session) = self.session {
match session.close().await { match session.close().await {
Ok(_) => { Ok(_) => {
info!(context, "close/expunge succeeded"); info!(context, "close/expunge succeeded");
@@ -45,40 +45,45 @@ impl Imap {
return Err(Error::NoSession); return Err(Error::NoSession);
} }
} }
let mut cfg = self.config.write().await; self.config.selected_folder = None;
cfg.selected_folder = None; self.config.selected_folder_needs_expunge = false;
cfg.selected_folder_needs_expunge = false;
Ok(()) Ok(())
} }
/// select a folder, possibly update uid_validity and, if needed, /// select a folder, possibly update uid_validity and, if needed,
/// expunge the folder to remove delete-marked messages. /// expunge the folder to remove delete-marked messages.
pub(super) async fn select_folder<S: AsRef<str>>( pub(super) async fn select_folder<S: AsRef<str>>(
&self, &mut self,
context: &Context, context: &Context,
folder: Option<S>, folder: Option<S>,
) -> Result<()> { ) -> Result<()> {
if self.session.lock().await.is_none() { if self.session.is_none() {
let mut cfg = self.config.write().await; self.config.selected_folder = None;
cfg.selected_folder = None; self.config.selected_folder_needs_expunge = false;
cfg.selected_folder_needs_expunge = false;
self.trigger_reconnect(); self.trigger_reconnect();
return Err(Error::NoSession); return Err(Error::NoSession);
} }
let needs_expunge = self.config.read().await.selected_folder_needs_expunge; // if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(ref folder) = folder {
if let Some(ref selected_folder) = self.config.selected_folder {
if folder.as_ref() == selected_folder {
return Ok(());
}
}
}
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
let needs_expunge = { self.config.selected_folder_needs_expunge };
if needs_expunge { if needs_expunge {
self.close_folder(context).await?; self.close_folder(context).await?;
} }
let folder_str: Option<&str> = folder.as_ref().map(|x| x.as_ref());
if self.config.read().await.selected_folder.as_deref() == folder_str {
return Ok(());
}
// select new folder // select new folder
if let Some(ref folder) = folder { if let Some(ref folder) = folder {
if let Some(ref mut session) = &mut *self.session.lock().await { if let Some(ref mut session) = &mut self.session {
let res = session.select(folder).await; let res = session.select(folder).await;
// https://tools.ietf.org/html/rfc3501#section-6.3.1 // https://tools.ietf.org/html/rfc3501#section-6.3.1
@@ -87,21 +92,20 @@ impl Imap {
match res { match res {
Ok(mailbox) => { Ok(mailbox) => {
let mut config = self.config.write().await; self.config.selected_folder = Some(folder.as_ref().to_string());
config.selected_folder = Some(folder.as_ref().to_string()); self.config.selected_mailbox = Some(mailbox);
config.selected_mailbox = Some(mailbox);
Ok(()) Ok(())
} }
Err(async_imap::error::Error::ConnectionLost) => { Err(async_imap::error::Error::ConnectionLost) => {
self.trigger_reconnect(); self.trigger_reconnect();
self.config.write().await.selected_folder = None; self.config.selected_folder = None;
Err(Error::ConnectionLost) Err(Error::ConnectionLost)
} }
Err(async_imap::error::Error::Validate(_)) => { Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.as_ref().to_string())) Err(Error::BadFolderName(folder.as_ref().to_string()))
} }
Err(err) => { Err(err) => {
self.config.write().await.selected_folder = None; self.config.selected_folder = None;
self.trigger_reconnect(); self.trigger_reconnect();
Err(Error::Other(err.to_string())) Err(Error::Other(err.to_string()))
} }

View File

@@ -1,172 +1,40 @@
use async_imap::{ use std::ops::{Deref, DerefMut};
error::Result as ImapResult,
types::{Capabilities, Fetch, Mailbox, Name}, use async_imap::Session as ImapSession;
Session as ImapSession,
};
use async_native_tls::TlsStream; use async_native_tls::TlsStream;
use async_std::net::TcpStream; use async_std::net::TcpStream;
use async_std::prelude::*;
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum Session { pub(crate) struct Session {
Secure(ImapSession<TlsStream<TcpStream>>), pub(super) inner: ImapSession<Box<dyn SessionStream>>,
Insecure(ImapSession<TcpStream>), }
pub(crate) trait SessionStream:
async_std::io::Read + async_std::io::Write + Unpin + Send + Sync + std::fmt::Debug
{
}
impl SessionStream for TlsStream<Box<dyn SessionStream>> {}
impl SessionStream for TlsStream<TcpStream> {}
impl SessionStream for TcpStream {}
impl Deref for Session {
type Target = ImapSession<Box<dyn SessionStream>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Session {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
} }
impl Session { impl Session {
pub async fn capabilities(&mut self) -> ImapResult<Capabilities> { pub fn idle(self) -> async_imap::extensions::idle::Handle<Box<dyn SessionStream>> {
let res = match self { let Session { inner } = self;
Session::Secure(i) => i.capabilities().await?, inner.idle()
Session::Insecure(i) => i.capabilities().await?,
};
Ok(res)
}
pub async fn list(
&mut self,
reference_name: Option<&str>,
mailbox_pattern: Option<&str>,
) -> ImapResult<Vec<Name>> {
let res = match self {
Session::Secure(i) => {
i.list(reference_name, mailbox_pattern)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.list(reference_name, mailbox_pattern)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn create<S: AsRef<str>>(&mut self, mailbox_name: S) -> ImapResult<()> {
match self {
Session::Secure(i) => i.create(mailbox_name).await?,
Session::Insecure(i) => i.create(mailbox_name).await?,
}
Ok(())
}
pub async fn subscribe<S: AsRef<str>>(&mut self, mailbox: S) -> ImapResult<()> {
match self {
Session::Secure(i) => i.subscribe(mailbox).await?,
Session::Insecure(i) => i.subscribe(mailbox).await?,
}
Ok(())
}
pub async fn close(&mut self) -> ImapResult<()> {
match self {
Session::Secure(i) => i.close().await?,
Session::Insecure(i) => i.close().await?,
}
Ok(())
}
pub async fn select<S: AsRef<str>>(&mut self, mailbox_name: S) -> ImapResult<Mailbox> {
let mbox = match self {
Session::Secure(i) => i.select(mailbox_name).await?,
Session::Insecure(i) => i.select(mailbox_name).await?,
};
Ok(mbox)
}
pub async fn fetch<S1, S2>(&mut self, sequence_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let res = match self {
Session::Secure(i) => {
i.fetch(sequence_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.fetch(sequence_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn uid_fetch<S1, S2>(&mut self, uid_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let res = match self {
Session::Secure(i) => {
i.uid_fetch(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.uid_fetch(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn uid_store<S1, S2>(&mut self, uid_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let res = match self {
Session::Secure(i) => {
i.uid_store(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.uid_store(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn uid_mv<S1: AsRef<str>, S2: AsRef<str>>(
&mut self,
uid_set: S1,
mailbox_name: S2,
) -> ImapResult<()> {
match self {
Session::Secure(i) => i.uid_mv(uid_set, mailbox_name).await?,
Session::Insecure(i) => i.uid_mv(uid_set, mailbox_name).await?,
}
Ok(())
}
pub async fn uid_copy<S1: AsRef<str>, S2: AsRef<str>>(
&mut self,
uid_set: S1,
mailbox_name: S2,
) -> ImapResult<()> {
match self {
Session::Secure(i) => i.uid_copy(uid_set, mailbox_name).await?,
Session::Insecure(i) => i.uid_copy(uid_set, mailbox_name).await?,
}
Ok(())
} }
} }

View File

@@ -1,9 +1,9 @@
//! # Import/export module //! # Import/export module
use core::cmp::{max, min}; use std::cmp::{max, min};
use std::path::Path;
use num_traits::FromPrimitive; use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use crate::blob::BlobObject; use crate::blob::BlobObject;
@@ -16,7 +16,6 @@ use crate::dc_tools::*;
use crate::e2ee; use crate::e2ee;
use crate::error::*; use crate::error::*;
use crate::events::Event; use crate::events::Event;
use crate::job::*;
use crate::key::{self, DcKey, Key, SignedSecretKey}; use crate::key::{self, DcKey, Key, SignedSecretKey};
use crate::message::{Message, MsgId}; use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage; use crate::mimeparser::SystemMessage;
@@ -53,13 +52,10 @@ pub enum ImexMode {
} }
/// Import/export things. /// Import/export things.
/// For this purpose, the function creates a job that is executed in the IMAP-thread then;
/// this requires to call dc_perform_inbox_jobs() regularly.
/// ///
/// What to do is defined by the *what* parameter. /// What to do is defined by the *what* parameter.
/// ///
/// While dc_imex() returns immediately, the started job may take a while, /// During execution of the job,
/// you can stop it using dc_stop_ongoing_process(). During execution of the job,
/// some events are sent out: /// some events are sent out:
/// ///
/// - A number of #DC_EVENT_IMEX_PROGRESS events are sent and may be used to create /// - A number of #DC_EVENT_IMEX_PROGRESS events are sent and may be used to create
@@ -68,41 +64,48 @@ pub enum ImexMode {
/// - For each file written on export, the function sends #DC_EVENT_IMEX_FILE_WRITTEN /// - For each file written on export, the function sends #DC_EVENT_IMEX_FILE_WRITTEN
/// ///
/// Only one import-/export-progress can run at the same time. /// Only one import-/export-progress can run at the same time.
/// To cancel an import-/export-progress, use dc_stop_ongoing_process(). /// To cancel an import-/export-progress, drop the future returned by this function.
pub fn imex(context: &Context, what: ImexMode, param1: Option<impl AsRef<Path>>) { pub async fn imex(
let mut param = Params::new(); context: &Context,
param.set_int(Param::Cmd, what as i32); what: ImexMode,
if let Some(param1) = param1 { param1: Option<impl AsRef<Path>>,
param.set(Param::Arg, param1.as_ref().to_string_lossy()); ) -> Result<()> {
} use futures::future::FutureExt;
job_kill_action(context, Action::ImexImap); let cancel = context.alloc_ongoing().await?;
job_add(context, Action::ImexImap, 0, param, 0); let res = imex_inner(context, what, param1)
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
.await;
context.free_ongoing().await;
res
} }
/// Returns the filename of the backup found (otherwise an error) /// Returns the filename of the backup found (otherwise an error)
pub fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> { pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
let dir_name = dir_name.as_ref(); let dir_name = dir_name.as_ref();
let dir_iter = std::fs::read_dir(dir_name)?; let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
let mut newest_backup_time = 0; let mut newest_backup_time = 0;
let mut newest_backup_path: Option<std::path::PathBuf> = None; let mut newest_backup_path: Option<PathBuf> = None;
for dirent in dir_iter { while let Some(dirent) = dir_iter.next().await {
if let Ok(dirent) = dirent { if let Ok(dirent) = dirent {
let path = dirent.path(); let path = dirent.path();
let name = dirent.file_name(); let name = dirent.file_name();
let name = name.to_string_lossy(); let name = name.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") { if name.starts_with("delta-chat") && name.ends_with(".bak") {
let sql = Sql::new(); let sql = Sql::new();
if sql.open(context, &path, true) { if sql.open(context, &path, true).await {
let curr_backup_time = sql let curr_backup_time = sql
.get_raw_config_int(context, "backup_time") .get_raw_config_int(context, "backup_time")
.await
.unwrap_or_default(); .unwrap_or_default();
if curr_backup_time > newest_backup_time { if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path); newest_backup_path = Some(path);
newest_backup_time = curr_backup_time; newest_backup_time = curr_backup_time;
} }
info!(context, "backup_time of {} is {}", name, curr_backup_time); info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close(&context); sql.close().await;
} }
} }
} }
@@ -113,28 +116,32 @@ pub fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<Strin
} }
} }
pub fn initiate_key_transfer(context: &Context) -> Result<String> { pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
ensure!(context.alloc_ongoing(), "could not allocate ongoing"); use futures::future::FutureExt;
let res = do_initiate_key_transfer(context);
context.free_ongoing(); let cancel = context.alloc_ongoing().await?;
let res = do_initiate_key_transfer(context)
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
.await;
context.free_ongoing().await;
res res
} }
fn do_initiate_key_transfer(context: &Context) -> Result<String> { async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
let mut msg: Message; let mut msg: Message;
let setup_code = create_setup_code(context); let setup_code = create_setup_code(context);
/* this may require a keypair to be created. this may take a second ... */ /* this may require a keypair to be created. this may take a second ... */
ensure!(!context.shall_stop_ongoing(), "canceled"); let setup_file_content = render_setup_file(context, &setup_code).await?;
let setup_file_content = render_setup_file(context, &setup_code)?;
/* encrypting may also take a while ... */ /* encrypting may also take a while ... */
ensure!(!context.shall_stop_ongoing(), "canceled");
let setup_file_blob = BlobObject::create( let setup_file_blob = BlobObject::create(
context, context,
"autocrypt-setup-message.html", "autocrypt-setup-message.html",
setup_file_content.as_bytes(), setup_file_content.as_bytes(),
)?; )
.await?;
let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF)?; let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF).await?;
msg = Message::default(); msg = Message::default();
msg.viewtype = Viewtype::File; msg.viewtype = Viewtype::File;
msg.param.set(Param::File, setup_file_blob.as_name()); msg.param.set(Param::File, setup_file_blob.as_name());
@@ -147,12 +154,11 @@ fn do_initiate_key_transfer(context: &Context) -> Result<String> {
ForcePlaintext::NoAutocryptHeader as i32, ForcePlaintext::NoAutocryptHeader as i32,
); );
ensure!(!context.shall_stop_ongoing(), "canceled"); let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
let msg_id = chat::send_msg(context, chat_id, &mut msg)?;
info!(context, "Wait for setup message being sent ...",); info!(context, "Wait for setup message being sent ...",);
while !context.shall_stop_ongoing() { while !context.shall_stop_ongoing().await {
std::thread::sleep(std::time::Duration::from_secs(1)); async_std::task::sleep(std::time::Duration::from_secs(1)).await;
if let Ok(msg) = Message::load_from_db(context, msg_id) { if let Ok(msg) = Message::load_from_db(context, msg_id).await {
if msg.is_sent() { if msg.is_sent() {
info!(context, "... setup message sent.",); info!(context, "... setup message sent.",);
break; break;
@@ -170,18 +176,18 @@ fn do_initiate_key_transfer(context: &Context) -> Result<String> {
/// Renders HTML body of a setup file message. /// Renders HTML body of a setup file message.
/// ///
/// The `passphrase` must be at least 2 characters long. /// The `passphrase` must be at least 2 characters long.
pub fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> { pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
ensure!( ensure!(
passphrase.len() >= 2, passphrase.len() >= 2,
"Passphrase must be at least 2 chars long." "Passphrase must be at least 2 chars long."
); );
let private_key = Key::from(SignedSecretKey::load_self(context)?); let private_key = Key::from(SignedSecretKey::load_self(context).await?);
let ac_headers = match context.get_config_bool(Config::E2eeEnabled) { let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
false => None, false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")), true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
}; };
let private_key_asc = private_key.to_asc(ac_headers); let private_key_asc = private_key.to_asc(ac_headers);
let encr = pgp::symm_encrypt(&passphrase, private_key_asc.as_bytes())?; let encr = pgp::symm_encrypt(&passphrase, private_key_asc.as_bytes()).await?;
let replacement = format!( let replacement = format!(
concat!( concat!(
@@ -193,8 +199,8 @@ pub fn render_setup_file(context: &Context, passphrase: &str) -> Result<String>
); );
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement); let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
let msg_subj = context.stock_str(StockMessage::AcSetupMsgSubject); let msg_subj = context.stock_str(StockMessage::AcSetupMsgSubject).await;
let msg_body = context.stock_str(StockMessage::AcSetupMsgBody); let msg_body = context.stock_str(StockMessage::AcSetupMsgBody).await;
let msg_body_html = msg_body.replace("\r", "").replace("\n", "<br>"); let msg_body_html = msg_body.replace("\r", "").replace("\n", "<br>");
Ok(format!( Ok(format!(
concat!( concat!(
@@ -237,8 +243,8 @@ pub fn create_setup_code(_context: &Context) -> String {
ret ret
} }
fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> { async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool(context, "bcc_self") { if !context.sql.get_raw_config_bool(context, "bcc_self").await {
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled. // TODO: define this as a stockstring once the wording is settled.
msg.text = Some( msg.text = Some(
@@ -247,26 +253,30 @@ fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
go to the settings and enable \"Send copy to self\"." go to the settings and enable \"Send copy to self\"."
.to_string(), .to_string(),
); );
chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg))?; chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg)).await?;
} }
Ok(()) Ok(())
} }
pub fn continue_key_transfer(context: &Context, msg_id: MsgId, setup_code: &str) -> Result<()> { pub async fn continue_key_transfer(
context: &Context,
msg_id: MsgId,
setup_code: &str,
) -> Result<()> {
ensure!(!msg_id.is_special(), "wrong id"); ensure!(!msg_id.is_special(), "wrong id");
let msg = Message::load_from_db(context, msg_id)?; let msg = Message::load_from_db(context, msg_id).await?;
ensure!( ensure!(
msg.is_setupmessage(), msg.is_setupmessage(),
"Message is no Autocrypt Setup Message." "Message is no Autocrypt Setup Message."
); );
if let Some(filename) = msg.get_file(context) { if let Some(filename) = msg.get_file(context) {
let file = dc_open_file(context, filename)?; let file = dc_open_file_std(context, filename)?;
let sc = normalize_setup_code(setup_code); let sc = normalize_setup_code(setup_code);
let armored_key = decrypt_setup_file(context, &sc, file)?; let armored_key = decrypt_setup_file(&sc, file).await?;
set_self_key(context, &armored_key, true, true)?; set_self_key(context, &armored_key, true, true).await?;
maybe_add_bcc_self_device_msg(context)?; maybe_add_bcc_self_device_msg(context).await?;
Ok(()) Ok(())
} else { } else {
@@ -274,7 +284,7 @@ pub fn continue_key_transfer(context: &Context, msg_id: MsgId, setup_code: &str)
} }
} }
fn set_self_key( async fn set_self_key(
context: &Context, context: &Context,
armored: &str, armored: &str,
set_default: bool, set_default: bool,
@@ -299,7 +309,8 @@ fn set_self_key(
}; };
context context
.sql .sql
.set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)?; .set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)
.await?;
} }
None => { None => {
if prefer_encrypt_required { if prefer_encrypt_required {
@@ -308,7 +319,7 @@ fn set_self_key(
} }
}; };
let self_addr = context.get_config(Config::ConfiguredAddr); let self_addr = context.get_config(Config::ConfiguredAddr).await;
ensure!(self_addr.is_some(), "Missing self addr"); ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?; let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
@@ -329,16 +340,16 @@ fn set_self_key(
} else { } else {
key::KeyPairUse::ReadOnly key::KeyPairUse::ReadOnly
}, },
)?; )
.await?;
Ok(()) Ok(())
} }
fn decrypt_setup_file<T: std::io::Read + std::io::Seek>( async fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
_context: &Context,
passphrase: &str, passphrase: &str,
file: T, file: T,
) -> Result<String> { ) -> Result<String> {
let plain_bytes = pgp::symm_decrypt(passphrase, file)?; let plain_bytes = pgp::symm_decrypt(passphrase, file).await?;
let plain_text = std::string::String::from_utf8(plain_bytes)?; let plain_text = std::string::String::from_utf8(plain_bytes)?;
Ok(plain_text) Ok(plain_text)
@@ -357,52 +368,50 @@ pub fn normalize_setup_code(s: &str) -> String {
out out
} }
#[allow(non_snake_case)] async fn imex_inner(
pub fn JobImexImap(context: &Context, job: &Job) -> Result<()> { context: &Context,
ensure!(context.alloc_ongoing(), "could not allocate ongoing"); what: ImexMode,
let what: Option<ImexMode> = job.param.get_int(Param::Cmd).and_then(ImexMode::from_i32); param: Option<impl AsRef<Path>>,
let param = job.param.get(Param::Arg).unwrap_or_default(); ) -> Result<()> {
ensure!(param.is_some(), "No Import/export dir/file given.");
ensure!(!param.is_empty(), "No Import/export dir/file given.");
info!(context, "Import/export process started."); info!(context, "Import/export process started.");
context.call_cb(Event::ImexProgress(10)); context.emit_event(Event::ImexProgress(10));
ensure!(context.sql.is_open(), "Database not opened."); ensure!(context.sql.is_open().await, "Database not opened.");
if what == Some(ImexMode::ExportBackup) || what == Some(ImexMode::ExportSelfKeys) {
let path = param.unwrap();
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
// before we export anything, make sure the private key exists // before we export anything, make sure the private key exists
if e2ee::ensure_secret_key_exists(context).is_err() { if e2ee::ensure_secret_key_exists(context).await.is_err() {
context.free_ongoing();
bail!("Cannot create private key or private key not available."); bail!("Cannot create private key or private key not available.");
} else { } else {
dc_create_folder(context, &param)?; dc_create_folder(context, &path).await?;
} }
} }
let path = Path::new(param);
let success = match what { let success = match what {
Some(ImexMode::ExportSelfKeys) => export_self_keys(context, path), ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
Some(ImexMode::ImportSelfKeys) => import_self_keys(context, path), ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
Some(ImexMode::ExportBackup) => export_backup(context, path), ImexMode::ExportBackup => export_backup(context, path).await,
Some(ImexMode::ImportBackup) => import_backup(context, path), ImexMode::ImportBackup => import_backup(context, path).await,
None => {
bail!("unknown IMEX type");
}
}; };
context.free_ongoing();
match success { match success {
Ok(()) => { Ok(()) => {
info!(context, "IMEX successfully completed"); info!(context, "IMEX successfully completed");
context.call_cb(Event::ImexProgress(1000)); context.emit_event(Event::ImexProgress(1000));
Ok(()) Ok(())
} }
Err(err) => { Err(err) => {
context.call_cb(Event::ImexProgress(0)); context.emit_event(Event::ImexProgress(0));
bail!("IMEX FAILED to complete: {}", err); bail!("IMEX FAILED to complete: {}", err);
} }
} }
} }
/// Import Backup /// Import Backup
fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> { async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
info!( info!(
context, context,
"Import \"{}\" to \"{}\".", "Import \"{}\" to \"{}\".",
@@ -411,84 +420,93 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
); );
ensure!( ensure!(
!context.is_configured(), !context.is_configured().await,
"Cannot import backups to accounts in use." "Cannot import backups to accounts in use."
); );
context.sql.close(&context); context.sql.close().await;
dc_delete_file(context, context.get_dbfile()); dc_delete_file(context, context.get_dbfile()).await;
ensure!( ensure!(
!context.get_dbfile().exists(), !context.get_dbfile().exists().await,
"Cannot delete old database." "Cannot delete old database."
); );
ensure!( ensure!(
dc_copy_file(context, backup_to_import.as_ref(), context.get_dbfile()), dc_copy_file(context, backup_to_import.as_ref(), context.get_dbfile()).await,
"could not copy file" "could not copy file"
); );
/* error already logged */ /* error already logged */
/* re-open copied database file */ /* re-open copied database file */
ensure!( ensure!(
context.sql.open(&context, &context.get_dbfile(), false), context
.sql
.open(&context, &context.get_dbfile(), false)
.await,
"could not re-open db" "could not re-open db"
); );
delete_and_reset_all_device_msgs(&context)?; delete_and_reset_all_device_msgs(&context).await?;
let total_files_cnt = context let total_files_cnt = context
.sql .sql
.query_get_value::<_, isize>(context, "SELECT COUNT(*) FROM backup_blobs;", params![]) .query_get_value::<isize>(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![])
.await
.unwrap_or_default() as usize; .unwrap_or_default() as usize;
info!( info!(
context, context,
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt, "***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
); );
let res = context.sql.query_map( let files = context
"SELECT file_name, file_content FROM backup_blobs ORDER BY id;", .sql
params![], .query_map(
|row| { "SELECT file_name, file_content FROM backup_blobs ORDER BY id;",
let name: String = row.get(0)?; paramsv![],
let blob: Vec<u8> = row.get(1)?; |row| {
let name: String = row.get(0)?;
let blob: Vec<u8> = row.get(1)?;
Ok((name, blob)) Ok((name, blob))
}, },
|files| { |files| {
for (processed_files_cnt, file) in files.enumerate() { files
let (file_name, file_blob) = file?; .collect::<std::result::Result<Vec<_>, _>>()
if context.shall_stop_ongoing() { .map_err(Into::into)
return Ok(false); },
} )
let mut permille = processed_files_cnt * 1000 / total_files_cnt; .await?;
if permille < 10 {
permille = 10
}
if permille > 990 {
permille = 990
}
context.call_cb(Event::ImexProgress(permille));
if file_blob.is_empty() {
continue;
}
let path_filename = context.get_blobdir().join(file_name); let mut all_files_extracted = true;
dc_write_file(context, &path_filename, &file_blob)?; for (processed_files_cnt, (file_name, file_blob)) in files.into_iter().enumerate() {
} if context.shall_stop_ongoing().await {
Ok(true) all_files_extracted = false;
}, break;
);
match res {
Ok(all_files_extracted) => {
if all_files_extracted {
// only delete backup_blobs if all files were successfully extracted
sql::execute(context, &context.sql, "DROP TABLE backup_blobs;", params![])?;
sql::try_execute(context, &context.sql, "VACUUM;").ok();
Ok(())
} else {
bail!("received stop signal");
}
} }
Err(err) => Err(err.into()), let mut permille = processed_files_cnt * 1000 / total_files_cnt;
if permille < 10 {
permille = 10
}
if permille > 990 {
permille = 990
}
context.emit_event(Event::ImexProgress(permille));
if file_blob.is_empty() {
continue;
}
let path_filename = context.get_blobdir().join(file_name);
dc_write_file(context, &path_filename, &file_blob).await?;
}
if all_files_extracted {
// only delete backup_blobs if all files were successfully extracted
context
.sql
.execute("DROP TABLE backup_blobs;", paramsv![])
.await?;
context.sql.execute("VACUUM;", paramsv![]).await.ok();
Ok(())
} else {
bail!("received stop signal");
} }
} }
@@ -497,28 +515,31 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
******************************************************************************/ ******************************************************************************/
/* the FILE_PROGRESS macro calls the callback with the permille of files processed. /* the FILE_PROGRESS macro calls the callback with the permille of files processed.
The macro avoids weird values of 0% or 100% while still working. */ The macro avoids weird values of 0% or 100% while still working. */
fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> { async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible) // get a fine backup file name (the name includes the date so that multiple backup instances are possible)
// FIXME: we should write to a temporary file first and rename it on success. this would guarantee the backup is complete. // FIXME: we should write to a temporary file first and rename it on success. this would guarantee the backup is complete.
// let dest_path_filename = dc_get_next_backup_file(context, dir, res); // let dest_path_filename = dc_get_next_backup_file(context, dir, res);
let now = time(); let now = time();
let dest_path_filename = dc_get_next_backup_path(dir, now)?; let dest_path_filename = dc_get_next_backup_path(dir, now).await?;
let dest_path_string = dest_path_filename.to_string_lossy().to_string(); let dest_path_string = dest_path_filename.to_string_lossy().to_string();
sql::housekeeping(context); sql::housekeeping(context).await;
sql::try_execute(context, &context.sql, "VACUUM;").ok(); context.sql.execute("VACUUM;", paramsv![]).await.ok();
// we close the database during the copy of the dbfile // we close the database during the copy of the dbfile
context.sql.close(context); context.sql.close().await;
info!( info!(
context, context,
"Backup '{}' to '{}'.", "Backup '{}' to '{}'.",
context.get_dbfile().display(), context.get_dbfile().display(),
dest_path_filename.display(), dest_path_filename.display(),
); );
let copied = dc_copy_file(context, context.get_dbfile(), &dest_path_filename); let copied = dc_copy_file(context, context.get_dbfile(), &dest_path_filename).await;
context.sql.open(&context, &context.get_dbfile(), false); context
.sql
.open(&context, &context.get_dbfile(), false)
.await;
if !copied { if !copied {
bail!( bail!(
@@ -529,86 +550,90 @@ fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
} }
let dest_sql = Sql::new(); let dest_sql = Sql::new();
ensure!( ensure!(
dest_sql.open(context, &dest_path_filename, false), dest_sql.open(context, &dest_path_filename, false).await,
"could not open exported database {}", "could not open exported database {}",
dest_path_string dest_path_string
); );
let res = match add_files_to_export(context, &dest_sql) { let res = match add_files_to_export(context, &dest_sql).await {
Err(err) => { Err(err) => {
dc_delete_file(context, &dest_path_filename); dc_delete_file(context, &dest_path_filename).await;
error!(context, "backup failed: {}", err); error!(context, "backup failed: {}", err);
Err(err) Err(err)
} }
Ok(()) => { Ok(()) => {
dest_sql.set_raw_config_int(context, "backup_time", now as i32)?; dest_sql
context.call_cb(Event::ImexFileWritten(dest_path_filename)); .set_raw_config_int(context, "backup_time", now as i32)
.await?;
context.emit_event(Event::ImexFileWritten(dest_path_filename));
Ok(()) Ok(())
} }
}; };
dest_sql.close(context); dest_sql.close().await;
Ok(res?) Ok(res?)
} }
fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> { async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
// add all files as blobs to the database copy (this does not require // add all files as blobs to the database copy (this does not require
// the source to be locked, neigher the destination as it is used only here) // the source to be locked, neigher the destination as it is used only here)
if !sql.table_exists("backup_blobs") { if !sql.table_exists("backup_blobs").await? {
sql::execute( sql.execute(
context,
&sql,
"CREATE TABLE backup_blobs (id INTEGER PRIMARY KEY, file_name, file_content);", "CREATE TABLE backup_blobs (id INTEGER PRIMARY KEY, file_name, file_content);",
params![], paramsv![],
)? )
.await?;
} }
// copy all files from BLOBDIR into backup-db // copy all files from BLOBDIR into backup-db
let mut total_files_cnt = 0; let mut total_files_cnt = 0;
let dir = context.get_blobdir(); let dir = context.get_blobdir();
let dir_handle = std::fs::read_dir(&dir)?; let dir_handle = async_std::fs::read_dir(&dir).await?;
total_files_cnt += dir_handle.filter(|r| r.is_ok()).count(); total_files_cnt += dir_handle.filter(|r| r.is_ok()).count().await;
info!(context, "EXPORT: total_files_cnt={}", total_files_cnt); info!(context, "EXPORT: total_files_cnt={}", total_files_cnt);
// scan directory, pass 2: copy files
let dir_handle = std::fs::read_dir(&dir)?;
let exported_all_files = sql.prepare(
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
|mut stmt, _| {
let mut processed_files_cnt = 0;
for entry in dir_handle {
let entry = entry?;
if context.shall_stop_ongoing() {
return Ok(false);
}
processed_files_cnt += 1;
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
context.call_cb(Event::ImexProgress(permille));
let name_f = entry.file_name(); sql.with_conn_async(|conn| async move {
let name = name_f.to_string_lossy(); // scan directory, pass 2: copy files
if name.starts_with("delta-chat") && name.ends_with(".bak") { let mut dir_handle = async_std::fs::read_dir(&dir).await?;
let mut processed_files_cnt = 0;
while let Some(entry) = dir_handle.next().await {
let entry = entry?;
if context.shall_stop_ongoing().await {
return Ok(());
}
processed_files_cnt += 1;
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
context.emit_event(Event::ImexProgress(permille));
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
continue;
}
info!(context, "EXPORT: copying filename={}", name);
let curr_path_filename = context.get_blobdir().join(entry.file_name());
if let Ok(buf) = dc_read_file(context, &curr_path_filename).await {
if buf.is_empty() {
continue; continue;
} }
info!(context, "EXPORT: copying filename={}", name); // bail out if we can't insert
let curr_path_filename = context.get_blobdir().join(entry.file_name()); let mut stmt = conn.prepare_cached(
if let Ok(buf) = dc_read_file(context, &curr_path_filename) { "INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
if buf.is_empty() { )?;
continue; stmt.execute(paramsv![name, buf])?;
}
// bail out if we can't insert
stmt.execute(params![name, buf])?;
}
} }
Ok(true) }
}, Ok(())
)?; })
ensure!(exported_all_files, "canceled during export-files"); .await?;
Ok(()) Ok(())
} }
/******************************************************************************* /*******************************************************************************
* Classic key import * Classic key import
******************************************************************************/ ******************************************************************************/
fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> { async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
/* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import /* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import
plain ASC keys, at least keys without a password, if we do not want to implement a password entry function. plain ASC keys, at least keys without a password, if we do not want to implement a password entry function.
Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation. Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation.
@@ -619,8 +644,8 @@ fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
let mut imported_cnt = 0; let mut imported_cnt = 0;
let dir_name = dir.as_ref().to_string_lossy(); let dir_name = dir.as_ref().to_string_lossy();
let dir_handle = std::fs::read_dir(&dir)?; let mut dir_handle = async_std::fs::read_dir(&dir).await?;
for entry in dir_handle { while let Some(entry) = dir_handle.next().await {
let entry_fn = entry?.file_name(); let entry_fn = entry?.file_name();
let name_f = entry_fn.to_string_lossy(); let name_f = entry_fn.to_string_lossy();
let path_plus_name = dir.as_ref().join(&entry_fn); let path_plus_name = dir.as_ref().join(&entry_fn);
@@ -640,10 +665,10 @@ fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
continue; continue;
} }
} }
match dc_read_file(context, &path_plus_name) { match dc_read_file(context, &path_plus_name).await {
Ok(buf) => { Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf); let armored = std::string::String::from_utf8_lossy(&buf);
if let Err(err) = set_self_key(context, &armored, set_default, false) { if let Err(err) = set_self_key(context, &armored, set_default, false).await {
error!(context, "set_self_key: {}", err); error!(context, "set_self_key: {}", err);
continue; continue;
} }
@@ -660,45 +685,54 @@ fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
Ok(()) Ok(())
} }
fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> { async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
let mut export_errors = 0; let mut export_errors = 0;
context.sql.query_map( let keys = context
"SELECT id, public_key, private_key, is_default FROM keypairs;", .sql
params![], .query_map(
|row| { "SELECT id, public_key, private_key, is_default FROM keypairs;",
let id = row.get(0)?; paramsv![],
let public_key_blob: Vec<u8> = row.get(1)?; |row| {
let public_key = Key::from_slice(&public_key_blob, KeyType::Public); let id = row.get(0)?;
let private_key_blob: Vec<u8> = row.get(2)?; let public_key_blob: Vec<u8> = row.get(1)?;
let private_key = Key::from_slice(&private_key_blob, KeyType::Private); let public_key = Key::from_slice(&public_key_blob, KeyType::Public);
let is_default: i32 = row.get(3)?; let private_key_blob: Vec<u8> = row.get(2)?;
let private_key = Key::from_slice(&private_key_blob, KeyType::Private);
let is_default: i32 = row.get(3)?;
Ok((id, public_key, private_key, is_default)) Ok((id, public_key, private_key, is_default))
}, },
|keys| { |keys| {
for key_pair in keys { keys.collect::<std::result::Result<Vec<_>, _>>()
let (id, public_key, private_key, is_default) = key_pair?; .map_err(Into::into)
let id = Some(id).filter(|_| is_default != 0); },
if let Some(key) = public_key { )
if export_key_to_asc_file(context, &dir, id, &key).is_err() { .await?;
export_errors += 1;
} for (id, public_key, private_key, is_default) in keys {
} else { let id = Some(id).filter(|_| is_default != 0);
export_errors += 1; if let Ok(key) = public_key {
} if export_key_to_asc_file(context, &dir, id, &key)
if let Some(key) = private_key { .await
if export_key_to_asc_file(context, &dir, id, &key).is_err() { .is_err()
export_errors += 1; {
} export_errors += 1;
} else {
export_errors += 1;
}
} }
} else {
Ok(()) export_errors += 1;
}, }
)?; if let Ok(key) = private_key {
if export_key_to_asc_file(context, &dir, id, &key)
.await
.is_err()
{
export_errors += 1;
}
} else {
export_errors += 1;
}
}
ensure!(export_errors == 0, "errors while exporting keys"); ensure!(export_errors == 0, "errors while exporting keys");
Ok(()) Ok(())
@@ -707,7 +741,7 @@ fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
/******************************************************************************* /*******************************************************************************
* Classic key export * Classic key export
******************************************************************************/ ******************************************************************************/
fn export_key_to_asc_file( async fn export_key_to_asc_file(
context: &Context, context: &Context,
dir: impl AsRef<Path>, dir: impl AsRef<Path>,
id: Option<i64>, id: Option<i64>,
@@ -720,13 +754,13 @@ fn export_key_to_asc_file(
dir.as_ref().join(format!("{}-key-{}.asc", kind, &id)) dir.as_ref().join(format!("{}-key-{}.asc", kind, &id))
}; };
info!(context, "Exporting key {}", file_name.display()); info!(context, "Exporting key {}", file_name.display());
dc_delete_file(context, &file_name); dc_delete_file(context, &file_name).await;
let res = key.write_asc_to_file(&file_name, context); let res = key.write_asc_to_file(&file_name, context).await;
if res.is_err() { if res.is_err() {
error!(context, "Cannot write key to {}", file_name.display()); error!(context, "Cannot write key to {}", file_name.display());
} else { } else {
context.call_cb(Event::ImexFileWritten(file_name)); context.emit_event(Event::ImexFileWritten(file_name));
} }
res res
} }
@@ -738,12 +772,12 @@ mod tests {
use crate::test_utils::*; use crate::test_utils::*;
use ::pgp::armor::BlockType; use ::pgp::armor::BlockType;
#[test] #[async_std::test]
fn test_render_setup_file() { async fn test_render_setup_file() {
let t = test_context(Some(Box::new(logging_cb))); let t = test_context().await;
configure_alice_keypair(&t.ctx); configure_alice_keypair(&t.ctx).await;
let msg = render_setup_file(&t.ctx, "hello").unwrap(); let msg = render_setup_file(&t.ctx, "hello").await.unwrap();
println!("{}", &msg); println!("{}", &msg);
// Check some substrings, indicating things got substituted. // Check some substrings, indicating things got substituted.
// In particular note the mixing of `\r\n` and `\n` depending // In particular note the mixing of `\r\n` and `\n` depending
@@ -757,21 +791,22 @@ mod tests {
assert!(msg.contains("-----END PGP MESSAGE-----\n")); assert!(msg.contains("-----END PGP MESSAGE-----\n"));
} }
#[test] #[async_std::test]
fn test_render_setup_file_newline_replace() { async fn test_render_setup_file_newline_replace() {
let t = dummy_context(); let t = dummy_context().await;
t.ctx t.ctx
.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string()) .set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
.await
.unwrap(); .unwrap();
configure_alice_keypair(&t.ctx); configure_alice_keypair(&t.ctx).await;
let msg = render_setup_file(&t.ctx, "pw").unwrap(); let msg = render_setup_file(&t.ctx, "pw").await.unwrap();
println!("{}", &msg); println!("{}", &msg);
assert!(msg.contains("<p>hello<br>there</p>")); assert!(msg.contains("<p>hello<br>there</p>"));
} }
#[test] #[async_std::test]
fn test_create_setup_code() { async fn test_create_setup_code() {
let t = dummy_context(); let t = dummy_context().await;
let setupcode = create_setup_code(&t.ctx); let setupcode = create_setup_code(&t.ctx);
assert_eq!(setupcode.len(), 44); assert_eq!(setupcode.len(), 44);
assert_eq!(setupcode.chars().nth(4).unwrap(), '-'); assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
@@ -784,15 +819,17 @@ mod tests {
assert_eq!(setupcode.chars().nth(39).unwrap(), '-'); assert_eq!(setupcode.chars().nth(39).unwrap(), '-');
} }
#[test] #[async_std::test]
fn test_export_key_to_asc_file() { async fn test_export_key_to_asc_file() {
let context = dummy_context(); let context = dummy_context().await;
let key = Key::from(alice_keypair().public); let key = Key::from(alice_keypair().public);
let blobdir = "$BLOBDIR"; let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key).is_ok()); assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
.await
.is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap(); let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{}/public-key-default.asc", blobdir); let filename = format!("{}/public-key-default.asc", blobdir);
let bytes = std::fs::read(&filename).unwrap(); let bytes = async_std::fs::read(&filename).await.unwrap();
assert_eq!(bytes, key.to_asc(None).into_bytes()); assert_eq!(bytes, key.to_asc(None).into_bytes());
} }
@@ -812,11 +849,8 @@ mod tests {
const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597"; const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597";
const S_EM_SETUPFILE: &str = include_str!("../test-data/message/stress.txt"); const S_EM_SETUPFILE: &str = include_str!("../test-data/message/stress.txt");
#[test] #[async_std::test]
fn test_split_and_decrypt() { async fn test_split_and_decrypt() {
let ctx = dummy_context();
let context = &ctx.ctx;
let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec(); let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec();
let (typ, headers, base64) = split_armored_data(&buf_1).unwrap(); let (typ, headers, base64) = split_armored_data(&buf_1).unwrap();
assert_eq!(typ, BlockType::Message); assert_eq!(typ, BlockType::Message);
@@ -826,12 +860,10 @@ mod tests {
assert!(!base64.is_empty()); assert!(!base64.is_empty());
let setup_file = S_EM_SETUPFILE.to_string(); let setup_file = S_EM_SETUPFILE.to_string();
let decrypted = decrypt_setup_file( let decrypted =
context, decrypt_setup_file(S_EM_SETUPCODE, std::io::Cursor::new(setup_file.as_bytes()))
S_EM_SETUPCODE, .await
std::io::Cursor::new(setup_file.as_bytes()), .unwrap();
)
.unwrap();
let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap(); let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap();

1150
src/job.rs

File diff suppressed because it is too large Load Diff

View File

@@ -1,203 +0,0 @@
use std::sync::{Arc, Condvar, Mutex};
use crate::context::Context;
use crate::error::{format_err, Result};
use crate::imap::Imap;
#[derive(Debug)]
pub struct JobThread {
pub name: &'static str,
pub folder_config_name: &'static str,
pub imap: Imap,
pub state: Arc<(Mutex<JobState>, Condvar)>,
}
#[derive(Clone, Debug, Default)]
pub struct JobState {
idle: bool,
jobs_needed: bool,
suspended: bool,
using_handle: bool,
}
impl JobThread {
pub fn new(name: &'static str, folder_config_name: &'static str, imap: Imap) -> Self {
JobThread {
name,
folder_config_name,
imap,
state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
}
}
pub fn suspend(&self, context: &Context) {
info!(context, "Suspending {}-thread.", self.name,);
{
self.state.0.lock().unwrap().suspended = true;
}
self.interrupt_idle(context);
loop {
let using_handle = self.state.0.lock().unwrap().using_handle;
if !using_handle {
return;
}
std::thread::sleep(std::time::Duration::from_micros(300 * 1000));
}
}
pub fn unsuspend(&self, context: &Context) {
info!(context, "Unsuspending {}-thread.", self.name);
let &(ref lock, ref cvar) = &*self.state.clone();
let mut state = lock.lock().unwrap();
state.suspended = false;
state.idle = true;
cvar.notify_one();
}
pub fn interrupt_idle(&self, context: &Context) {
{
self.state.0.lock().unwrap().jobs_needed = true;
}
info!(context, "Interrupting {}-IDLE...", self.name);
self.imap.interrupt_idle(context);
let &(ref lock, ref cvar) = &*self.state.clone();
let mut state = lock.lock().unwrap();
state.idle = true;
cvar.notify_one();
info!(context, "Interrupting {}-IDLE... finished", self.name);
}
pub async fn fetch(&mut self, context: &Context, use_network: bool) {
{
let &(ref lock, _) = &*self.state.clone();
let mut state = lock.lock().unwrap();
if state.suspended {
return;
}
state.using_handle = true;
}
if use_network {
if let Err(err) = self.connect_and_fetch(context).await {
warn!(context, "connect+fetch failed: {}, reconnect & retry", err);
self.imap.trigger_reconnect();
if let Err(err) = self.connect_and_fetch(context).await {
warn!(context, "connect+fetch failed: {}", err);
}
}
}
self.state.0.lock().unwrap().using_handle = false;
}
async fn connect_and_fetch(&mut self, context: &Context) -> Result<()> {
let prefix = format!("{}-fetch", self.name);
self.imap.connect_configured(context)?;
if let Some(watch_folder) = self.get_watch_folder(context) {
let start = std::time::Instant::now();
info!(context, "{} started...", prefix);
let res = self
.imap
.fetch(context, &watch_folder)
.await
.map_err(Into::into);
let elapsed = start.elapsed().as_millis();
info!(context, "{} done in {:.3} ms.", prefix, elapsed);
res
} else {
Err(format_err!("WatchFolder not found: not-set"))
}
}
fn get_watch_folder(&self, context: &Context) -> Option<String> {
match context.sql.get_raw_config(context, self.folder_config_name) {
Some(name) => Some(name),
None => {
if self.folder_config_name == "configured_inbox_folder" {
// initialized with old version, so has not set configured_inbox_folder
Some("INBOX".to_string())
} else {
None
}
}
}
}
pub fn idle(&self, context: &Context, use_network: bool) {
{
let &(ref lock, ref cvar) = &*self.state.clone();
let mut state = lock.lock().unwrap();
if state.jobs_needed {
info!(
context,
"{}-IDLE will not be started as it was interrupted while not idling.",
self.name,
);
state.jobs_needed = false;
return;
}
if state.suspended {
while !state.idle {
state = cvar.wait(state).unwrap();
}
state.idle = false;
return;
}
state.using_handle = true;
if !use_network {
state.using_handle = false;
while !state.idle {
state = cvar.wait(state).unwrap();
}
state.idle = false;
return;
}
}
let prefix = format!("{}-IDLE", self.name);
let do_fake_idle = match self.imap.connect_configured(context) {
Ok(()) => {
if !self.imap.can_idle() {
true // we have to do fake_idle
} else {
let watch_folder = self.get_watch_folder(context);
info!(context, "{} started...", prefix);
let res = self.imap.idle(context, watch_folder);
info!(context, "{} ended...", prefix);
if let Err(err) = res {
warn!(context, "{} failed: {} -> reconnecting", prefix, err);
// something is borked, let's start afresh on the next occassion
self.imap.disconnect(context);
}
false
}
}
Err(err) => {
info!(context, "{}-IDLE connection fail: {:?}", self.name, err);
// if the connection fails, use fake_idle to retry periodically
// fake_idle() will be woken up by interrupt_idle() as
// well so will act on maybe_network events
true
}
};
if do_fake_idle {
let watch_folder = self.get_watch_folder(context);
self.imap.fake_idle(context, watch_folder);
}
self.state.0.lock().unwrap().using_handle = false;
}
}

View File

@@ -2,8 +2,9 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::io::Cursor; use std::io::Cursor;
use std::path::Path;
use async_std::path::Path;
use async_trait::async_trait;
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use pgp::composed::Deserializable; use pgp::composed::Deserializable;
use pgp::ser::Serialize; use pgp::ser::Serialize;
@@ -37,6 +38,8 @@ pub enum Error {
NoConfiguredAddr, NoConfiguredAddr,
#[error("Configured address is invalid: {}", _0)] #[error("Configured address is invalid: {}", _0)]
InvalidConfiguredAddr(#[from] InvalidEmailError), InvalidConfiguredAddr(#[from] InvalidEmailError),
#[error("no data provided")]
Empty,
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@@ -46,6 +49,7 @@ pub type Result<T> = std::result::Result<T, Error>;
/// This trait is implemented for rPGP's [SignedPublicKey] and /// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little /// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world. /// easier in the deltachat world.
#[async_trait]
pub trait DcKey: Serialize + Deserializable { pub trait DcKey: Serialize + Deserializable {
type KeyType: Serialize + Deserializable; type KeyType: Serialize + Deserializable;
@@ -65,7 +69,7 @@ pub trait DcKey: Serialize + Deserializable {
} }
/// Load the users' default key from the database. /// Load the users' default key from the database.
fn load_self(context: &Context) -> Result<Self::KeyType>; async fn load_self(context: &Context) -> Result<Self::KeyType>;
/// Serialise the key to a base64 string. /// Serialise the key to a base64 string.
fn to_base64(&self) -> String { fn to_base64(&self) -> String {
@@ -79,23 +83,28 @@ pub trait DcKey: Serialize + Deserializable {
} }
} }
#[async_trait]
impl DcKey for SignedPublicKey { impl DcKey for SignedPublicKey {
type KeyType = SignedPublicKey; type KeyType = SignedPublicKey;
fn load_self(context: &Context) -> Result<Self::KeyType> { async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context.sql.query_row( match context
r#" .sql
.query_row(
r#"
SELECT public_key SELECT public_key
FROM keypairs FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr") WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1; AND is_default=1;
"#, "#,
params![], paramsv![],
|row| row.get::<_, Vec<u8>>(0), |row| row.get::<_, Vec<u8>>(0),
) { )
.await
{
Ok(bytes) => Self::from_slice(&bytes), Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => { Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
let keypair = generate_keypair(context)?; let keypair = generate_keypair(context).await?;
Ok(keypair.public) Ok(keypair.public)
} }
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
@@ -103,23 +112,28 @@ impl DcKey for SignedPublicKey {
} }
} }
#[async_trait]
impl DcKey for SignedSecretKey { impl DcKey for SignedSecretKey {
type KeyType = SignedSecretKey; type KeyType = SignedSecretKey;
fn load_self(context: &Context) -> Result<Self::KeyType> { async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context.sql.query_row( match context
r#" .sql
.query_row(
r#"
SELECT private_key SELECT private_key
FROM keypairs FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr") WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1; AND is_default=1;
"#, "#,
params![], paramsv![],
|row| row.get::<_, Vec<u8>>(0), |row| row.get::<_, Vec<u8>>(0),
) { )
.await
{
Ok(bytes) => Self::from_slice(&bytes), Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => { Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
let keypair = generate_keypair(context)?; let keypair = generate_keypair(context).await?;
Ok(keypair.secret) Ok(keypair.secret)
} }
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
@@ -127,24 +141,29 @@ impl DcKey for SignedSecretKey {
} }
} }
fn generate_keypair(context: &Context) -> Result<KeyPair> { async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context let addr = context
.get_config(Config::ConfiguredAddr) .get_config(Config::ConfiguredAddr)
.await
.ok_or_else(|| Error::NoConfiguredAddr)?; .ok_or_else(|| Error::NoConfiguredAddr)?;
let addr = EmailAddress::new(&addr)?; let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().unwrap(); let _guard = context.generating_key_mutex.lock().await;
// Check if the key appeared while we were waiting on the lock. // Check if the key appeared while we were waiting on the lock.
match context.sql.query_row( match context
r#" .sql
.query_row(
r#"
SELECT public_key, private_key SELECT public_key, private_key
FROM keypairs FROM keypairs
WHERE addr=?1 WHERE addr=?1
AND is_default=1; AND is_default=1;
"#, "#,
params![addr], paramsv![addr],
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)), |row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
) { )
.await
{
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair { Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
addr, addr,
public: SignedPublicKey::from_slice(&pub_bytes)?, public: SignedPublicKey::from_slice(&pub_bytes)?,
@@ -152,11 +171,13 @@ fn generate_keypair(context: &Context) -> Result<KeyPair> {
}), }),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => { Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType)) let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
.unwrap_or_default(); .unwrap_or_default();
info!(context, "Generating keypair with type {}", keytype); info!(context, "Generating keypair with type {}", keytype);
let keypair = crate::pgp::create_keypair(addr, keytype)?; let keypair =
store_self_keypair(context, &keypair, KeyPairUse::Default)?; async_std::task::spawn_blocking(move || crate::pgp::create_keypair(addr, keytype))
.await?;
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
info!( info!(
context, context,
"Keypair generated in {:.3}s.", "Keypair generated in {:.3}s.",
@@ -243,22 +264,17 @@ impl Key {
!self.is_public() !self.is_public()
} }
pub fn from_slice(bytes: &[u8], key_type: KeyType) -> Option<Self> { pub fn from_slice(bytes: &[u8], key_type: KeyType) -> Result<Self> {
if bytes.is_empty() { if bytes.is_empty() {
return None; return Err(Error::Empty);
} }
let res: std::result::Result<Key, _> = match key_type {
KeyType::Public => SignedPublicKey::from_bytes(Cursor::new(bytes)).map(Into::into), let res = match key_type {
KeyType::Private => SignedSecretKey::from_bytes(Cursor::new(bytes)).map(Into::into), KeyType::Public => SignedPublicKey::from_bytes(Cursor::new(bytes))?.into(),
KeyType::Private => SignedSecretKey::from_bytes(Cursor::new(bytes))?.into(),
}; };
match res { Ok(res)
Ok(key) => Some(key),
Err(err) => {
eprintln!("Invalid key bytes: {:?}", err);
None
}
}
} }
pub fn from_armored_string( pub fn from_armored_string(
@@ -323,14 +339,14 @@ impl Key {
.expect("failed to serialize key") .expect("failed to serialize key")
} }
pub fn write_asc_to_file( pub async fn write_asc_to_file(
&self, &self,
file: impl AsRef<Path>, file: impl AsRef<Path>,
context: &Context, context: &Context,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let file_content = self.to_asc(None).into_bytes(); let file_content = self.to_asc(None).into_bytes();
let res = dc_write_file(context, &file, &file_content); let res = dc_write_file(context, &file, &file_content).await;
if res.is_err() { if res.is_err() {
error!(context, "Cannot write key to {}", file.as_ref().display()); error!(context, "Cannot write key to {}", file.as_ref().display());
} }
@@ -402,7 +418,7 @@ impl SaveKeyError {
/// same key again overwrites it. /// same key again overwrites it.
/// ///
/// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr /// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr
pub fn store_self_keypair( pub async fn store_self_keypair(
context: &Context, context: &Context,
keypair: &KeyPair, keypair: &KeyPair,
default: KeyPairUse, default: KeyPairUse,
@@ -421,34 +437,37 @@ pub fn store_self_keypair(
.sql .sql
.execute( .execute(
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;", "DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
params![public_key, secret_key], paramsv![public_key, secret_key],
) )
.await
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?; .map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default { if default == KeyPairUse::Default {
context context
.sql .sql
.execute("UPDATE keypairs SET is_default=0;", params![]) .execute("UPDATE keypairs SET is_default=0;", paramsv![])
.await
.map_err(|err| SaveKeyError::new("failed to clear default", err))?; .map_err(|err| SaveKeyError::new("failed to clear default", err))?;
} }
let is_default = match default { let is_default = match default {
KeyPairUse::Default => true, KeyPairUse::Default => true as i32,
KeyPairUse::ReadOnly => false, KeyPairUse::ReadOnly => false as i32,
}; };
let addr = keypair.addr.to_string();
let t = time();
let params = paramsv![addr, is_default, public_key, secret_key, t];
context context
.sql .sql
.execute( .execute(
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created) "INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
VALUES (?,?,?,?,?);", VALUES (?,?,?,?,?);",
params![ params,
keypair.addr.to_string(),
is_default as i32,
public_key,
secret_key,
time()
],
) )
.map(|_| ()) .await
.map_err(|err| SaveKeyError::new("failed to insert keypair", err)) .map_err(|err| SaveKeyError::new("failed to insert keypair", err))?;
Ok(())
} }
/// Make a fingerprint human-readable, in hex format. /// Make a fingerprint human-readable, in hex format.
@@ -483,6 +502,7 @@ mod tests {
use crate::test_utils::*; use crate::test_utils::*;
use std::convert::TryFrom; use std::convert::TryFrom;
use async_std::sync::Arc;
use lazy_static::lazy_static; use lazy_static::lazy_static;
lazy_static! { lazy_static! {
@@ -604,58 +624,62 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
KeyType::Private KeyType::Private
}, },
); );
assert!(bad_key.is_none()); assert!(bad_key.is_err());
} }
} }
#[test] #[async_std::test]
fn test_load_self_existing() { async fn test_load_self_existing() {
let alice = alice_keypair(); let alice = alice_keypair();
let t = dummy_context(); let t = dummy_context().await;
configure_alice_keypair(&t.ctx); configure_alice_keypair(&t.ctx).await;
let pubkey = SignedPublicKey::load_self(&t.ctx).unwrap(); let pubkey = SignedPublicKey::load_self(&t.ctx).await.unwrap();
assert_eq!(alice.public, pubkey); assert_eq!(alice.public, pubkey);
let seckey = SignedSecretKey::load_self(&t.ctx).unwrap(); let seckey = SignedSecretKey::load_self(&t.ctx).await.unwrap();
assert_eq!(alice.secret, seckey); assert_eq!(alice.secret, seckey);
} }
#[test] #[async_std::test]
#[ignore] // generating keys is expensive #[ignore] // generating keys is expensive
fn test_load_self_generate_public() { async fn test_load_self_generate_public() {
let t = dummy_context(); let t = dummy_context().await;
t.ctx t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com")) .set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
.unwrap(); .unwrap();
let key = SignedPublicKey::load_self(&t.ctx); let key = SignedPublicKey::load_self(&t.ctx).await;
assert!(key.is_ok()); assert!(key.is_ok());
} }
#[test] #[async_std::test]
#[ignore] // generating keys is expensive #[ignore] // generating keys is expensive
fn test_load_self_generate_secret() { async fn test_load_self_generate_secret() {
let t = dummy_context(); let t = dummy_context().await;
t.ctx t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com")) .set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
.unwrap(); .unwrap();
let key = SignedSecretKey::load_self(&t.ctx); let key = SignedSecretKey::load_self(&t.ctx).await;
assert!(key.is_ok()); assert!(key.is_ok());
} }
#[test] #[async_std::test]
#[ignore] // generating keys is expensive #[ignore] // generating keys is expensive
fn test_load_self_generate_concurrent() { async fn test_load_self_generate_concurrent() {
use std::sync::Arc;
use std::thread; use std::thread;
let t = dummy_context(); let t = dummy_context().await;
t.ctx t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com")) .set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
.unwrap(); .unwrap();
let ctx = Arc::new(t.ctx); let ctx = t.ctx.clone();
let ctx0 = Arc::clone(&ctx); let ctx0 = ctx.clone();
let thr0 = thread::spawn(move || SignedPublicKey::load_self(&ctx0)); let thr0 =
let ctx1 = Arc::clone(&ctx); thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx0)));
let thr1 = thread::spawn(move || SignedPublicKey::load_self(&ctx1)); let ctx1 = ctx.clone();
let thr1 =
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx1)));
let res0 = thr0.join().unwrap(); let res0 = thr0.join().unwrap();
let res1 = thr1.join().unwrap(); let res1 = thr1.join().unwrap();
assert_eq!(res0.unwrap(), res1.unwrap()); assert_eq!(res0.unwrap(), res1.unwrap());
@@ -686,22 +710,29 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
assert_eq!(public.primary_key, KEYPAIR.public.primary_key); assert_eq!(public.primary_key, KEYPAIR.public.primary_key);
} }
#[test] #[async_std::test]
fn test_save_self_key_twice() { async fn test_save_self_key_twice() {
// Saving the same key twice should result in only one row in // Saving the same key twice should result in only one row in
// the keypairs table. // the keypairs table.
let t = dummy_context(); let t = dummy_context().await;
let nrows = || { let ctx = Arc::new(t.ctx);
t.ctx
.sql let ctx1 = ctx.clone();
.query_get_value::<_, u32>(&t.ctx, "SELECT COUNT(*) FROM keypairs;", params![]) let nrows = || async {
ctx1.sql
.query_get_value::<u32>(&ctx1, "SELECT COUNT(*) FROM keypairs;", paramsv![])
.await
.unwrap() .unwrap()
}; };
assert_eq!(nrows(), 0); assert_eq!(nrows().await, 0);
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap(); store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
assert_eq!(nrows(), 1); .await
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap(); .unwrap();
assert_eq!(nrows(), 1); assert_eq!(nrows().await, 1);
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
.await
.unwrap();
assert_eq!(nrows().await, 1);
} }
// Convenient way to create a new key if you need one, run with // Convenient way to create a new key if you need one, run with

View File

@@ -1,45 +1,47 @@
use std::borrow::Cow; use anyhow::Result;
use crate::constants::KeyType; use crate::constants::KeyType;
use crate::context::Context; use crate::context::Context;
use crate::key::Key; use crate::key::Key;
use crate::sql::Sql;
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct Keyring<'a> { pub struct Keyring {
keys: Vec<Cow<'a, Key>>, keys: Vec<Key>,
} }
impl<'a> Keyring<'a> { impl Keyring {
pub fn add_owned(&mut self, key: Key) { pub fn add(&mut self, key: Key) {
self.add(Cow::Owned(key)) self.keys.push(key)
} }
pub fn add_ref(&mut self, key: &'a Key) { pub fn len(&self) -> usize {
self.add(Cow::Borrowed(key)) self.keys.len()
} }
fn add(&mut self, key: Cow<'a, Key>) { pub fn is_empty(&self) -> bool {
self.keys.push(key); self.keys.is_empty()
} }
pub fn keys(&self) -> &[Cow<'a, Key>] { pub fn keys(&self) -> &[Key] {
&self.keys &self.keys
} }
pub fn load_self_private_for_decrypting( pub async fn load_self_private_for_decrypting(
&mut self,
context: &Context, context: &Context,
self_addr: impl AsRef<str>, self_addr: impl AsRef<str>,
sql: &Sql, ) -> Result<Self> {
) -> bool { let blob: Vec<u8> = context
sql.query_get_value( .sql
context, .query_get_value_result(
"SELECT private_key FROM keypairs ORDER BY addr=? DESC, is_default DESC;", "SELECT private_key FROM keypairs ORDER BY addr=? DESC, is_default DESC;",
&[self_addr.as_ref()], paramsv![self_addr.as_ref().to_string()],
) )
.and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Private)) .await?
.map(|key| self.add_owned(key)) .unwrap_or_default();
.is_some()
let key = async_std::task::spawn_blocking(move || Key::from_slice(&blob, KeyType::Private))
.await?;
Ok(Self { keys: vec![key] })
} }
} }

View File

@@ -14,11 +14,22 @@ extern crate strum_macros;
#[macro_use] #[macro_use]
extern crate debug_stub_derive; extern crate debug_stub_derive;
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
#[macro_use] #[macro_use]
pub mod log; pub mod log;
#[macro_use] #[macro_use]
pub mod error; pub mod error;
#[cfg(feature = "internals")]
#[macro_use]
pub mod sql;
#[cfg(not(feature = "internals"))]
#[macro_use]
mod sql;
pub mod headerdef; pub mod headerdef;
pub(crate) mod events; pub(crate) mod events;
@@ -36,9 +47,9 @@ pub mod context;
mod e2ee; mod e2ee;
mod imap; mod imap;
pub mod imex; pub mod imex;
mod scheduler;
#[macro_use] #[macro_use]
pub mod job; pub mod job;
mod job_thread;
pub mod key; pub mod key;
mod keyring; mod keyring;
pub mod location; pub mod location;
@@ -56,7 +67,6 @@ pub mod qr;
pub mod securejoin; pub mod securejoin;
mod simplify; mod simplify;
mod smtp; mod smtp;
pub mod sql;
pub mod stock; pub mod stock;
mod token; mod token;
#[macro_use] #[macro_use]

View File

@@ -10,11 +10,10 @@ use crate::context::*;
use crate::dc_tools::*; use crate::dc_tools::*;
use crate::error::{ensure, Error}; use crate::error::{ensure, Error};
use crate::events::Event; use crate::events::Event;
use crate::job::{self, job_action_exists, job_add, Job}; use crate::job::{self, Job};
use crate::message::{Message, MsgId}; use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage; use crate::mimeparser::SystemMessage;
use crate::param::*; use crate::param::*;
use crate::sql;
use crate::stock::StockMessage; use crate::stock::StockMessage;
/// Location record /// Location record
@@ -191,91 +190,103 @@ impl Kml {
} }
// location streaming // location streaming
pub fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) { pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) {
let now = time(); let now = time();
if !(seconds < 0 || chat_id.is_special()) { if !(seconds < 0 || chat_id.is_special()) {
let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id); let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id).await;
if sql::execute( if context
context, .sql
&context.sql, .execute(
"UPDATE chats \ "UPDATE chats \
SET locations_send_begin=?, \ SET locations_send_begin=?, \
locations_send_until=? \ locations_send_until=? \
WHERE id=?", WHERE id=?",
params![ paramsv![
if 0 != seconds { now } else { 0 }, if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 }, if 0 != seconds { now + seconds } else { 0 },
chat_id, chat_id,
], ],
) )
.is_ok() .await
.is_ok()
{ {
if 0 != seconds && !is_sending_locations_before { if 0 != seconds && !is_sending_locations_before {
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.text = msg.text = Some(
Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)); context
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled); .stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)
chat::send_msg(context, chat_id, &mut msg).unwrap_or_default(); .await,
} else if 0 == seconds && is_sending_locations_before {
let stock_str =
context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_info_msg(context, chat_id, stock_str);
}
context.call_cb(Event::ChatModified(chat_id));
if 0 != seconds {
schedule_MAYBE_SEND_LOCATIONS(context, false);
job_add(
context,
job::Action::MaybeSendLocationsEnded,
chat_id.to_u32() as i32,
Params::new(),
seconds + 1,
); );
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
chat::send_msg(context, chat_id, &mut msg)
.await
.unwrap_or_default();
} else if 0 == seconds && is_sending_locations_before {
let stock_str = context
.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0)
.await;
chat::add_info_msg(context, chat_id, stock_str).await;
}
context.emit_event(Event::ChatModified(chat_id));
if 0 != seconds {
schedule_maybe_send_locations(context, false).await;
job::add(
context,
job::Job::new(
job::Action::MaybeSendLocationsEnded,
chat_id.to_u32(),
Params::new(),
seconds + 1,
),
)
.await;
} }
} }
} }
} }
#[allow(non_snake_case)] async fn schedule_maybe_send_locations(context: &Context, force_schedule: bool) {
fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, force_schedule: bool) { if force_schedule || !job::action_exists(context, job::Action::MaybeSendLocations).await {
if force_schedule || !job_action_exists(context, job::Action::MaybeSendLocations) { job::add(
job_add(
context, context,
job::Action::MaybeSendLocations, job::Job::new(job::Action::MaybeSendLocations, 0, Params::new(), 60),
0, )
Params::new(), .await;
60,
);
}; };
} }
pub fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) -> bool { pub async fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) -> bool {
context context
.sql .sql
.exists( .exists(
"SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;", "SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;",
params![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()], paramsv![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
) )
.await
.unwrap_or_default() .unwrap_or_default()
} }
pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool { pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
if latitude == 0.0 && longitude == 0.0 { if latitude == 0.0 && longitude == 0.0 {
return true; return true;
} }
let mut continue_streaming = false; let mut continue_streaming = false;
if let Ok(chats) = context.sql.query_map( if let Ok(chats) = context
"SELECT id FROM chats WHERE locations_send_until>?;", .sql
params![time()], .query_map(
|row| row.get::<_, i32>(0), "SELECT id FROM chats WHERE locations_send_until>?;",
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into), paramsv![time()],
) { |row| row.get::<_, i32>(0),
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await
{
for chat_id in chats { for chat_id in chats {
if let Err(err) = context.sql.execute( if let Err(err) = context.sql.execute(
"INSERT INTO locations \ "INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);", (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
params![ paramsv![
latitude, latitude,
longitude, longitude,
accuracy, accuracy,
@@ -283,22 +294,22 @@ pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> b
chat_id, chat_id,
DC_CONTACT_ID_SELF, DC_CONTACT_ID_SELF,
] ]
) { ).await {
warn!(context, "failed to store location {:?}", err); warn!(context, "failed to store location {:?}", err);
} else { } else {
continue_streaming = true; continue_streaming = true;
} }
} }
if continue_streaming { if continue_streaming {
context.call_cb(Event::LocationChanged(Some(DC_CONTACT_ID_SELF))); context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
}; };
schedule_MAYBE_SEND_LOCATIONS(context, false); schedule_maybe_send_locations(context, false).await;
} }
continue_streaming continue_streaming
} }
pub fn get_range( pub async fn get_range(
context: &Context, context: &Context,
chat_id: ChatId, chat_id: ChatId,
contact_id: u32, contact_id: u32,
@@ -317,7 +328,7 @@ pub fn get_range(
AND (? OR l.from_id=?) \ AND (? OR l.from_id=?) \
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \ AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;", ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
params![ paramsv![
if chat_id.is_unset() { 1 } else { 0 }, if chat_id.is_unset() { 1 } else { 0 },
chat_id, chat_id,
if contact_id == 0 { 1 } else { 0 }, if contact_id == 0 { 1 } else { 0 },
@@ -356,6 +367,7 @@ pub fn get_range(
Ok(ret) Ok(ret)
}, },
) )
.await
.unwrap_or_default() .unwrap_or_default()
} }
@@ -364,28 +376,33 @@ fn is_marker(txt: &str) -> bool {
} }
/// Deletes all locations from the database. /// Deletes all locations from the database.
pub fn delete_all(context: &Context) -> Result<(), Error> { pub async fn delete_all(context: &Context) -> Result<(), Error> {
sql::execute(context, &context.sql, "DELETE FROM locations;", params![])?; context
context.call_cb(Event::LocationChanged(None)); .sql
.execute("DELETE FROM locations;", paramsv![])
.await?;
context.emit_event(Event::LocationChanged(None));
Ok(()) Ok(())
} }
pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Error> { pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Error> {
let mut last_added_location_id = 0; let mut last_added_location_id = 0;
let self_addr = context let self_addr = context
.get_config(Config::ConfiguredAddr) .get_config(Config::ConfiguredAddr)
.await
.unwrap_or_default(); .unwrap_or_default();
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row( let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;", "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
params![chat_id], |row| { paramsv![chat_id], |row| {
let send_begin: i64 = row.get(0)?; let send_begin: i64 = row.get(0)?;
let send_until: i64 = row.get(1)?; let send_until: i64 = row.get(1)?;
let last_sent: i64 = row.get(2)?; let last_sent: i64 = row.get(2)?;
Ok((send_begin, send_until, last_sent)) Ok((send_begin, send_until, last_sent))
})?; })
.await?;
let now = time(); let now = time();
let mut location_count = 0; let mut location_count = 0;
@@ -404,7 +421,7 @@ pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Erro
AND independent=0 \ AND independent=0 \
GROUP BY timestamp \ GROUP BY timestamp \
ORDER BY timestamp;", ORDER BY timestamp;",
params![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF], paramsv![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF],
|row| { |row| {
let location_id: i32 = row.get(0)?; let location_id: i32 = row.get(0)?;
let latitude: f64 = row.get(1)?; let latitude: f64 = row.get(1)?;
@@ -429,7 +446,7 @@ pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Erro
} }
Ok(()) Ok(())
} }
)?; ).await?;
ret += "</Document>\n</kml>"; ret += "</Document>\n</kml>";
} }
@@ -462,37 +479,38 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String
) )
} }
pub fn set_kml_sent_timestamp( pub async fn set_kml_sent_timestamp(
context: &Context, context: &Context,
chat_id: ChatId, chat_id: ChatId,
timestamp: i64, timestamp: i64,
) -> Result<(), Error> { ) -> Result<(), Error> {
sql::execute( context
context, .sql
&context.sql, .execute(
"UPDATE chats SET locations_last_sent=? WHERE id=?;", "UPDATE chats SET locations_last_sent=? WHERE id=?;",
params![timestamp, chat_id], paramsv![timestamp, chat_id],
)?; )
.await?;
Ok(()) Ok(())
} }
pub fn set_msg_location_id( pub async fn set_msg_location_id(
context: &Context, context: &Context,
msg_id: MsgId, msg_id: MsgId,
location_id: u32, location_id: u32,
) -> Result<(), Error> { ) -> Result<(), Error> {
sql::execute( context
context, .sql
&context.sql, .execute(
"UPDATE msgs SET location_id=? WHERE id=?;", "UPDATE msgs SET location_id=? WHERE id=?;",
params![location_id, msg_id], paramsv![location_id, msg_id],
)?; )
.await?;
Ok(()) Ok(())
} }
pub fn save( pub async fn save(
context: &Context, context: &Context,
chat_id: ChatId, chat_id: ChatId,
contact_id: u32, contact_id: u32,
@@ -500,54 +518,66 @@ pub fn save(
independent: bool, independent: bool,
) -> Result<u32, Error> { ) -> Result<u32, Error> {
ensure!(!chat_id.is_special(), "Invalid chat id"); ensure!(!chat_id.is_special(), "Invalid chat id");
context
.sql let mut newest_timestamp = 0;
.prepare2( let mut newest_location_id = 0;
"SELECT id FROM locations WHERE timestamp=? AND from_id=?",
"INSERT INTO locations\ for location in locations {
let &Location {
timestamp,
latitude,
longitude,
accuracy,
..
} = location;
context
.sql
.with_conn(move |mut conn| {
let mut stmt_test = conn
.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
let mut stmt_insert = conn.prepare_cached(
"INSERT INTO locations\
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \ (timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
VALUES (?,?,?,?,?,?,?);", VALUES (?,?,?,?,?,?,?);",
|mut stmt_test, mut stmt_insert, conn| { )?;
let mut newest_timestamp = 0;
let mut newest_location_id = 0;
for location in locations { let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
let exists =
stmt_test.exists(params![location.timestamp, contact_id as i32])?;
if independent || !exists { if independent || !exists {
stmt_insert.execute(params![ stmt_insert.execute(paramsv![
location.timestamp, timestamp,
contact_id as i32,
chat_id,
latitude,
longitude,
accuracy,
independent,
])?;
if timestamp > newest_timestamp {
// okay to drop, as we use cached prepared statements
drop(stmt_test);
drop(stmt_insert);
newest_timestamp = timestamp;
newest_location_id = crate::sql::get_rowid2(
&mut conn,
"locations",
"timestamp",
timestamp,
"from_id",
contact_id as i32, contact_id as i32,
chat_id, )?;
location.latitude,
location.longitude,
location.accuracy,
independent,
])?;
if location.timestamp > newest_timestamp {
newest_timestamp = location.timestamp;
newest_location_id = sql::get_rowid2_with_conn(
context,
conn,
"locations",
"timestamp",
location.timestamp,
"from_id",
contact_id as i32,
);
}
} }
} }
Ok(newest_location_id) Ok(())
}, })
) .await?;
.map_err(Into::into) }
Ok(newest_location_id)
} }
#[allow(non_snake_case)] pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> job::Status {
pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
let now = time(); let now = time();
let mut continue_streaming = false; let mut continue_streaming = false;
info!( info!(
@@ -555,101 +585,118 @@ pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Statu
" ----------------- MAYBE_SEND_LOCATIONS -------------- ", " ----------------- MAYBE_SEND_LOCATIONS -------------- ",
); );
if let Ok(rows) = context.sql.query_map( let rows = context
"SELECT id, locations_send_begin, locations_last_sent \ .sql
.query_map(
"SELECT id, locations_send_begin, locations_last_sent \
FROM chats \ FROM chats \
WHERE locations_send_until>?;", WHERE locations_send_until>?;",
params![now], paramsv![now],
|row| { |row| {
let chat_id: ChatId = row.get(0)?; let chat_id: ChatId = row.get(0)?;
let locations_send_begin: i64 = row.get(1)?; let locations_send_begin: i64 = row.get(1)?;
let locations_last_sent: i64 = row.get(2)?; let locations_last_sent: i64 = row.get(2)?;
continue_streaming = true; continue_streaming = true;
// be a bit tolerant as the timer may not align exactly with time(NULL) // be a bit tolerant as the timer may not align exactly with time(NULL)
if now - locations_last_sent < (60 - 3) { if now - locations_last_sent < (60 - 3) {
Ok(None) Ok(None)
} else { } else {
Ok(Some((chat_id, locations_send_begin, locations_last_sent))) Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
} }
}, },
|rows| { |rows| {
rows.filter_map(|v| v.transpose()) rows.filter_map(|v| v.transpose())
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map_err(Into::into) .map_err(Into::into)
}, },
) { )
.await;
if rows.is_ok() {
let msgs = context let msgs = context
.sql .sql
.prepare( .with_conn(move |conn| {
"SELECT id \ let rows = rows.unwrap();
let mut stmt_locations = conn.prepare_cached(
"SELECT id \
FROM locations \ FROM locations \
WHERE from_id=? \ WHERE from_id=? \
AND timestamp>=? \ AND timestamp>=? \
AND timestamp>? \ AND timestamp>? \
AND independent=0 \ AND independent=0 \
ORDER BY timestamp;", ORDER BY timestamp;",
|mut stmt_locations, _| { )?;
let msgs = rows
.into_iter() let mut msgs = Vec::new();
.filter_map(|(chat_id, locations_send_begin, locations_last_sent)| { for (chat_id, locations_send_begin, locations_last_sent) in &rows {
if !stmt_locations if !stmt_locations
.exists(params![ .exists(paramsv![
DC_CONTACT_ID_SELF, DC_CONTACT_ID_SELF,
locations_send_begin, *locations_send_begin,
locations_last_sent, *locations_last_sent,
]) ])
.unwrap_or_default() .unwrap_or_default()
{ {
// if there is no new location, there's nothing to send. // if there is no new location, there's nothing to send.
// however, maybe we want to bypass this test eg. 15 minutes // however, maybe we want to bypass this test eg. 15 minutes
None } else {
} else { // pending locations are attached automatically to every message,
// pending locations are attached automatically to every message, // so also to this empty text message.
// so also to this empty text message. // DC_CMD_LOCATION is only needed to create a nicer subject.
// DC_CMD_LOCATION is only needed to create a nicer subject. //
// // for optimisation and to avoid flooding the sending queue,
// for optimisation and to avoid flooding the sending queue, // we could sending these messages only if we're really online.
// we could sending these messages only if we're really online. // the easiest way to determine this, is to check for an empty message queue.
// the easiest way to determine this, is to check for an empty message queue. // (might not be 100%, however, as positions are sent combined later
// (might not be 100%, however, as positions are sent combined later // and dc_set_location() is typically called periodically, this is ok)
// and dc_set_location() is typically called periodically, this is ok) let mut msg = Message::new(Viewtype::Text);
let mut msg = Message::new(Viewtype::Text); msg.hidden = true;
msg.hidden = true; msg.param.set_cmd(SystemMessage::LocationOnly);
msg.param.set_cmd(SystemMessage::LocationOnly); msgs.push((*chat_id, msg));
Some((chat_id, msg)) }
} }
})
.collect::<Vec<_>>(); Ok(msgs)
Ok(msgs) })
}, .await
) .unwrap_or_default();
.unwrap_or_default(); // TODO: Better error handling
for (chat_id, mut msg) in msgs.into_iter() { for (chat_id, mut msg) in msgs.into_iter() {
// TODO: better error handling // TODO: better error handling
chat::send_msg(context, chat_id, &mut msg).unwrap_or_default(); chat::send_msg(context, chat_id, &mut msg)
.await
.unwrap_or_default();
} }
} }
if continue_streaming { if continue_streaming {
schedule_MAYBE_SEND_LOCATIONS(context, true); schedule_maybe_send_locations(context, true).await;
} }
job::Status::Finished(Ok(())) job::Status::Finished(Ok(()))
} }
#[allow(non_snake_case)] pub(crate) async fn job_maybe_send_locations_ended(
pub(crate) fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status { context: &Context,
job: &mut Job,
) -> job::Status {
// this function is called when location-streaming _might_ have ended for a chat. // this function is called when location-streaming _might_ have ended for a chat.
// the function checks, if location-streaming is really ended; // the function checks, if location-streaming is really ended;
// if so, a device-message is added if not yet done. // if so, a device-message is added if not yet done.
let chat_id = ChatId::new(job.foreign_id); let chat_id = ChatId::new(job.foreign_id);
let (send_begin, send_until) = job_try!(context.sql.query_row( let (send_begin, send_until) = job_try!(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?", context
params![chat_id], .sql
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)), .query_row(
)); "SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
paramsv![chat_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
)
.await
);
if !(send_begin != 0 && time() <= send_until) { if !(send_begin != 0 && time() <= send_until) {
// still streaming - // still streaming -
@@ -659,12 +706,14 @@ pub(crate) fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> jo
// not streaming, device-message already sent // not streaming, device-message already sent
job_try!(context.sql.execute( job_try!(context.sql.execute(
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?", "UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
params![chat_id], paramsv![chat_id],
)); ).await);
let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0); let stock_str = context
chat::add_info_msg(context, chat_id, stock_str); .stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0)
context.call_cb(Event::ChatModified(chat_id)); .await;
chat::add_info_msg(context, chat_id, stock_str).await;
context.emit_event(Event::ChatModified(chat_id));
} }
} }
job::Status::Finished(Ok(())) job::Status::Finished(Ok(()))
@@ -675,9 +724,9 @@ mod tests {
use super::*; use super::*;
use crate::test_utils::dummy_context; use crate::test_utils::dummy_context;
#[test] #[async_std::test]
fn test_kml_parse() { async fn test_kml_parse() {
let context = dummy_context(); let context = dummy_context().await;
let xml = let xml =
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>"; b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";

View File

@@ -7,9 +7,7 @@ macro_rules! info {
}; };
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{ ($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*); let formatted = format!($msg, $($args),*);
let thread = ::std::thread::current(); let full = format!("{file}:{line}: {msg}",
let full = format!("{thid:?} {file}:{line}: {msg}",
thid = thread.id(),
file = file!(), file = file!(),
line = line!(), line = line!(),
msg = &formatted); msg = &formatted);
@@ -24,9 +22,7 @@ macro_rules! warn {
}; };
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{ ($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*); let formatted = format!($msg, $($args),*);
let thread = ::std::thread::current(); let full = format!("{file}:{line}: {msg}",
let full = format!("{thid:?} {file}:{line}: {msg}",
thid = thread.id(),
file = file!(), file = file!(),
line = line!(), line = line!(),
msg = &formatted); msg = &formatted);
@@ -48,6 +44,6 @@ macro_rules! error {
#[macro_export] #[macro_export]
macro_rules! emit_event { macro_rules! emit_event {
($ctx:expr, $event:expr) => { ($ctx:expr, $event:expr) => {
$ctx.call_cb($event); $ctx.emit_event($event);
}; };
} }

View File

@@ -50,59 +50,69 @@ impl LoginParam {
} }
/// Read the login parameters from the database. /// Read the login parameters from the database.
pub fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self { pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
let prefix = prefix.as_ref(); let prefix = prefix.as_ref();
let sql = &context.sql; let sql = &context.sql;
let key = format!("{}addr", prefix); let key = format!("{}addr", prefix);
let addr = sql let addr = sql
.get_raw_config(context, key) .get_raw_config(context, key)
.await
.unwrap_or_default() .unwrap_or_default()
.trim() .trim()
.to_string(); .to_string();
let key = format!("{}mail_server", prefix); let key = format!("{}mail_server", prefix);
let mail_server = sql.get_raw_config(context, key).unwrap_or_default(); let mail_server = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}mail_port", prefix); let key = format!("{}mail_port", prefix);
let mail_port = sql.get_raw_config_int(context, key).unwrap_or_default(); let mail_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let key = format!("{}mail_user", prefix); let key = format!("{}mail_user", prefix);
let mail_user = sql.get_raw_config(context, key).unwrap_or_default(); let mail_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}mail_pw", prefix); let key = format!("{}mail_pw", prefix);
let mail_pw = sql.get_raw_config(context, key).unwrap_or_default(); let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}imap_certificate_checks", prefix); let key = format!("{}imap_certificate_checks", prefix);
let imap_certificate_checks = let imap_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key) { if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap() num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else { } else {
Default::default() Default::default()
}; };
let key = format!("{}send_server", prefix); let key = format!("{}send_server", prefix);
let send_server = sql.get_raw_config(context, key).unwrap_or_default(); let send_server = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}send_port", prefix); let key = format!("{}send_port", prefix);
let send_port = sql.get_raw_config_int(context, key).unwrap_or_default(); let send_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let key = format!("{}send_user", prefix); let key = format!("{}send_user", prefix);
let send_user = sql.get_raw_config(context, key).unwrap_or_default(); let send_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}send_pw", prefix); let key = format!("{}send_pw", prefix);
let send_pw = sql.get_raw_config(context, key).unwrap_or_default(); let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}smtp_certificate_checks", prefix); let key = format!("{}smtp_certificate_checks", prefix);
let smtp_certificate_checks = let smtp_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key) { if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap() num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else { } else {
Default::default() Default::default()
}; };
let key = format!("{}server_flags", prefix); let key = format!("{}server_flags", prefix);
let server_flags = sql.get_raw_config_int(context, key).unwrap_or_default(); let server_flags = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
LoginParam { LoginParam {
addr, addr,
@@ -125,7 +135,7 @@ impl LoginParam {
} }
/// Save this loginparam to the database. /// Save this loginparam to the database.
pub fn save_to_database( pub async fn save_to_database(
&self, &self,
context: &Context, context: &Context,
prefix: impl AsRef<str>, prefix: impl AsRef<str>,
@@ -134,40 +144,49 @@ impl LoginParam {
let sql = &context.sql; let sql = &context.sql;
let key = format!("{}addr", prefix); let key = format!("{}addr", prefix);
sql.set_raw_config(context, key, Some(&self.addr))?; sql.set_raw_config(context, key, Some(&self.addr)).await?;
let key = format!("{}mail_server", prefix); let key = format!("{}mail_server", prefix);
sql.set_raw_config(context, key, Some(&self.mail_server))?; sql.set_raw_config(context, key, Some(&self.mail_server))
.await?;
let key = format!("{}mail_port", prefix); let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(context, key, self.mail_port)?; sql.set_raw_config_int(context, key, self.mail_port).await?;
let key = format!("{}mail_user", prefix); let key = format!("{}mail_user", prefix);
sql.set_raw_config(context, key, Some(&self.mail_user))?; sql.set_raw_config(context, key, Some(&self.mail_user))
.await?;
let key = format!("{}mail_pw", prefix); let key = format!("{}mail_pw", prefix);
sql.set_raw_config(context, key, Some(&self.mail_pw))?; sql.set_raw_config(context, key, Some(&self.mail_pw))
.await?;
let key = format!("{}imap_certificate_checks", prefix); let key = format!("{}imap_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.imap_certificate_checks as i32)?; sql.set_raw_config_int(context, key, self.imap_certificate_checks as i32)
.await?;
let key = format!("{}send_server", prefix); let key = format!("{}send_server", prefix);
sql.set_raw_config(context, key, Some(&self.send_server))?; sql.set_raw_config(context, key, Some(&self.send_server))
.await?;
let key = format!("{}send_port", prefix); let key = format!("{}send_port", prefix);
sql.set_raw_config_int(context, key, self.send_port)?; sql.set_raw_config_int(context, key, self.send_port).await?;
let key = format!("{}send_user", prefix); let key = format!("{}send_user", prefix);
sql.set_raw_config(context, key, Some(&self.send_user))?; sql.set_raw_config(context, key, Some(&self.send_user))
.await?;
let key = format!("{}send_pw", prefix); let key = format!("{}send_pw", prefix);
sql.set_raw_config(context, key, Some(&self.send_pw))?; sql.set_raw_config(context, key, Some(&self.send_pw))
.await?;
let key = format!("{}smtp_certificate_checks", prefix); let key = format!("{}smtp_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.smtp_certificate_checks as i32)?; sql.set_raw_config_int(context, key, self.smtp_certificate_checks as i32)
.await?;
let key = format!("{}server_flags", prefix); let key = format!("{}server_flags", prefix);
sql.set_raw_config_int(context, key, self.server_flags)?; sql.set_raw_config_int(context, key, self.server_flags)
.await?;
Ok(()) Ok(())
} }

File diff suppressed because it is too large Load Diff

View File

@@ -66,74 +66,89 @@ pub struct RenderedEmail {
} }
impl<'a, 'b> MimeFactory<'a, 'b> { impl<'a, 'b> MimeFactory<'a, 'b> {
pub fn from_msg( pub async fn from_msg(
context: &'a Context, context: &'a Context,
msg: &'b Message, msg: &'b Message,
attach_selfavatar: bool, attach_selfavatar: bool,
) -> Result<MimeFactory<'a, 'b>, Error> { ) -> Result<MimeFactory<'a, 'b>, Error> {
let chat = Chat::load_from_db(context, msg.chat_id)?; let chat = Chat::load_from_db(context, msg.chat_id).await?;
let from_addr = context let from_addr = context
.get_config(Config::ConfiguredAddr) .get_config(Config::ConfiguredAddr)
.await
.unwrap_or_default();
let from_displayname = context
.get_config(Config::Displayname)
.await
.unwrap_or_default(); .unwrap_or_default();
let from_displayname = context.get_config(Config::Displayname).unwrap_or_default();
let mut recipients = Vec::with_capacity(5); let mut recipients = Vec::with_capacity(5);
let mut req_mdn = false; let mut req_mdn = false;
if chat.is_self_talk() { if chat.is_self_talk() {
recipients.push((from_displayname.to_string(), from_addr.to_string())); recipients.push((from_displayname.to_string(), from_addr.to_string()));
} else { } else {
context.sql.query_map( context
"SELECT c.authname, c.addr \ .sql
.query_map(
"SELECT c.authname, c.addr \
FROM chats_contacts cc \ FROM chats_contacts cc \
LEFT JOIN contacts c ON cc.contact_id=c.id \ LEFT JOIN contacts c ON cc.contact_id=c.id \
WHERE cc.chat_id=? AND cc.contact_id>9;", WHERE cc.chat_id=? AND cc.contact_id>9;",
params![msg.chat_id], paramsv![msg.chat_id],
|row| { |row| {
let authname: String = row.get(0)?; let authname: String = row.get(0)?;
let addr: String = row.get(1)?; let addr: String = row.get(1)?;
Ok((authname, addr)) Ok((authname, addr))
}, },
|rows| { |rows| {
for row in rows { for row in rows {
let (authname, addr) = row?; let (authname, addr) = row?;
if !recipients_contain_addr(&recipients, &addr) { if !recipients_contain_addr(&recipients, &addr) {
recipients.push((authname, addr)); recipients.push((authname, addr));
}
} }
} Ok(())
Ok(()) },
}, )
)?; .await?;
let command = msg.param.get_cmd(); let command = msg.param.get_cmd();
if command != SystemMessage::AutocryptSetupMessage if command != SystemMessage::AutocryptSetupMessage
&& command != SystemMessage::SecurejoinMessage && command != SystemMessage::SecurejoinMessage
&& context.get_config_bool(Config::MdnsEnabled) && context.get_config_bool(Config::MdnsEnabled).await
{ {
req_mdn = true; req_mdn = true;
} }
} }
let (in_reply_to, references) = context.sql.query_row( let (in_reply_to, references) = context
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?", .sql
params![msg.id], .query_row(
|row| { "SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
let in_reply_to: String = row.get(0)?; paramsv![msg.id],
let references: String = row.get(1)?; |row| {
let in_reply_to: String = row.get(0)?;
let references: String = row.get(1)?;
Ok(( Ok((
render_rfc724_mid_list(&in_reply_to), render_rfc724_mid_list(&in_reply_to),
render_rfc724_mid_list(&references), render_rfc724_mid_list(&references),
)) ))
}, },
)?; )
.await?;
let default_str = context
.stock_str(StockMessage::StatusLine)
.await
.to_string();
let factory = MimeFactory { let factory = MimeFactory {
from_addr, from_addr,
from_displayname, from_displayname,
selfstatus: context selfstatus: context
.get_config(Config::Selfstatus) .get_config(Config::Selfstatus)
.unwrap_or_else(|| context.stock_str(StockMessage::StatusLine).to_string()), .await
.unwrap_or_else(|| default_str),
recipients, recipients,
timestamp: msg.timestamp_sort, timestamp: msg.timestamp_sort,
loaded: Loaded::Message { chat }, loaded: Loaded::Message { chat },
@@ -148,29 +163,42 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Ok(factory) Ok(factory)
} }
pub fn from_mdn( pub async fn from_mdn(
context: &'a Context, context: &'a Context,
msg: &'b Message, msg: &'b Message,
additional_msg_ids: Vec<String>, additional_msg_ids: Vec<String>,
) -> Result<Self, Error> { ) -> Result<MimeFactory<'a, 'b>, Error> {
ensure!(!msg.chat_id.is_special(), "Invalid chat id"); ensure!(!msg.chat_id.is_special(), "Invalid chat id");
let contact = Contact::load_from_db(context, msg.from_id)?; let contact = Contact::load_from_db(context, msg.from_id).await?;
let from_addr = context
.get_config(Config::ConfiguredAddr)
.await
.unwrap_or_default();
let from_displayname = context
.get_config(Config::Displayname)
.await
.unwrap_or_default();
let default_str = context
.stock_str(StockMessage::StatusLine)
.await
.to_string();
let selfstatus = context
.get_config(Config::Selfstatus)
.await
.unwrap_or_else(|| default_str);
let timestamp = dc_create_smeared_timestamp(context).await;
Ok(MimeFactory { let res = MimeFactory::<'a, 'b> {
context, context,
from_addr: context from_addr,
.get_config(Config::ConfiguredAddr) from_displayname,
.unwrap_or_default(), selfstatus,
from_displayname: context.get_config(Config::Displayname).unwrap_or_default(),
selfstatus: context
.get_config(Config::Selfstatus)
.unwrap_or_else(|| context.stock_str(StockMessage::StatusLine).to_string()),
recipients: vec![( recipients: vec![(
contact.get_authname().to_string(), contact.get_authname().to_string(),
contact.get_addr().to_string(), contact.get_addr().to_string(),
)], )],
timestamp: dc_create_smeared_timestamp(context), timestamp,
loaded: Loaded::MDN { additional_msg_ids }, loaded: Loaded::MDN { additional_msg_ids },
msg, msg,
in_reply_to: String::default(), in_reply_to: String::default(),
@@ -178,26 +206,31 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
req_mdn: false, req_mdn: false,
last_added_location_id: 0, last_added_location_id: 0,
attach_selfavatar: false, attach_selfavatar: false,
}) };
Ok(res)
} }
fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate>, &str)>, Error> { async fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate<'_>>, &str)>, Error> {
let self_addr = self let self_addr = self
.context .context
.get_config(Config::ConfiguredAddr) .get_config(Config::ConfiguredAddr)
.await
.ok_or_else(|| format_err!("Not configured"))?; .ok_or_else(|| format_err!("Not configured"))?;
Ok(self let mut res = Vec::new();
for (_, addr) in self
.recipients .recipients
.iter() .iter()
.filter(|(_, addr)| addr != &self_addr) .filter(|(_, addr)| addr != &self_addr)
.map(|(_, addr)| { {
( res.push((
Peerstate::from_addr(self.context, &self.context.sql, addr), Peerstate::from_addr(self.context, addr).await,
addr.as_str(), addr.as_str(),
) ));
}) }
.collect())
Ok(res)
} }
fn is_e2ee_guaranteed(&self) -> bool { fn is_e2ee_guaranteed(&self) -> bool {
@@ -257,11 +290,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
} }
} }
fn should_do_gossip(&self) -> bool { async fn should_do_gossip(&self) -> bool {
match &self.loaded { match &self.loaded {
Loaded::Message { chat } => { Loaded::Message { chat } => {
// beside key- and member-changes, force re-gossip every 48 hours // beside key- and member-changes, force re-gossip every 48 hours
let gossiped_timestamp = chat.get_gossiped_timestamp(self.context); let gossiped_timestamp = chat.get_gossiped_timestamp(self.context).await;
if time() > gossiped_timestamp + (2 * 24 * 60 * 60) { if time() > gossiped_timestamp + (2 * 24 * 60 * 60) {
return true; return true;
} }
@@ -302,12 +335,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
} }
} }
fn subject_str(&self) -> String { async fn subject_str(&self) -> String {
match self.loaded { match self.loaded {
Loaded::Message { ref chat } => { Loaded::Message { ref chat } => {
if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage { if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
self.context self.context
.stock_str(StockMessage::AcSetupMsgSubject) .stock_str(StockMessage::AcSetupMsgSubject)
.await
.into_owned() .into_owned()
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup { } else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
let re = if self.in_reply_to.is_empty() { let re = if self.in_reply_to.is_empty() {
@@ -323,12 +357,17 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
&self.msg.param, &self.msg.param,
32, 32,
self.context, self.context,
); )
.await;
let raw_subject = raw.lines().next().unwrap_or_default(); let raw_subject = raw.lines().next().unwrap_or_default();
format!("Chat: {}", raw_subject) format!("Chat: {}", raw_subject)
} }
} }
Loaded::MDN { .. } => self.context.stock_str(StockMessage::ReadRcpt).into_owned(), Loaded::MDN { .. } => self
.context
.stock_str(StockMessage::ReadRcpt)
.await
.into_owned(),
} }
} }
@@ -339,7 +378,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.collect() .collect()
} }
pub fn render(mut self) -> Result<RenderedEmail, Error> { pub async fn render(mut self) -> Result<RenderedEmail, Error> {
// Headers that are encrypted // Headers that are encrypted
// - Chat-*, except Chat-Version // - Chat-*, except Chat-Version
// - Secure-Join* // - Secure-Join*
@@ -422,17 +461,18 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let min_verified = self.min_verified(); let min_verified = self.min_verified();
let grpimage = self.grpimage(); let grpimage = self.grpimage();
let force_plaintext = self.should_force_plaintext(); let force_plaintext = self.should_force_plaintext();
let subject_str = self.subject_str(); let subject_str = self.subject_str().await;
let e2ee_guaranteed = self.is_e2ee_guaranteed(); let e2ee_guaranteed = self.is_e2ee_guaranteed();
let mut encrypt_helper = EncryptHelper::new(self.context)?; let encrypt_helper = EncryptHelper::new(self.context).await?;
let subject = encode_words(&subject_str); let subject = encode_words(&subject_str);
let mut message = match self.loaded { let mut message = match self.loaded {
Loaded::Message { .. } => { Loaded::Message { .. } => {
self.render_message(&mut protected_headers, &mut unprotected_headers, &grpimage)? self.render_message(&mut protected_headers, &mut unprotected_headers, &grpimage)
.await?
} }
Loaded::MDN { .. } => self.render_mdn()?, Loaded::MDN { .. } => self.render_mdn().await?,
}; };
if force_plaintext != ForcePlaintext::NoAutocryptHeader as i32 { if force_plaintext != ForcePlaintext::NoAutocryptHeader as i32 {
@@ -443,7 +483,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
protected_headers.push(Header::new("Subject".into(), subject)); protected_headers.push(Header::new("Subject".into(), subject));
let peerstates = self.peerstates_for_recipients()?; let peerstates = self.peerstates_for_recipients().await?;
let should_encrypt = let should_encrypt =
encrypt_helper.should_encrypt(self.context, e2ee_guaranteed, &peerstates)?; encrypt_helper.should_encrypt(self.context, e2ee_guaranteed, &peerstates)?;
let is_encrypted = should_encrypt && force_plaintext == 0; let is_encrypted = should_encrypt && force_plaintext == 0;
@@ -471,7 +511,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let outer_message = if is_encrypted { let outer_message = if is_encrypted {
// Add gossip headers in chats with multiple recipients // Add gossip headers in chats with multiple recipients
if peerstates.len() > 1 && self.should_do_gossip() { if peerstates.len() > 1 && self.should_do_gossip().await {
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) { for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
if peerstate.peek_key(min_verified).is_some() { if peerstate.peek_key(min_verified).is_some() {
if let Some(header) = peerstate.render_gossip_header(min_verified) { if let Some(header) = peerstate.render_gossip_header(min_verified) {
@@ -519,8 +559,9 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
println!("{}", raw_message); println!("{}", raw_message);
} }
let encrypted = let encrypted = encrypt_helper
encrypt_helper.encrypt(self.context, min_verified, message, &peerstates)?; .encrypt(self.context, min_verified, message, peerstates)
.await?;
outer_message = outer_message outer_message = outer_message
.child( .child(
@@ -592,9 +633,9 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Some(part) Some(part)
} }
fn get_location_kml_part(&mut self) -> Result<PartBuilder, Error> { async fn get_location_kml_part(&mut self) -> Result<PartBuilder, Error> {
let (kml_content, last_added_location_id) = let (kml_content, last_added_location_id) =
location::get_kml(self.context, self.msg.chat_id)?; location::get_kml(self.context, self.msg.chat_id).await?;
let part = PartBuilder::new() let part = PartBuilder::new()
.content_type( .content_type(
&"application/vnd.google-earth.kml+xml" &"application/vnd.google-earth.kml+xml"
@@ -614,7 +655,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
} }
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
fn render_message( async fn render_message(
&mut self, &mut self,
protected_headers: &mut Vec<Header>, protected_headers: &mut Vec<Header>,
unprotected_headers: &mut Vec<Header>, unprotected_headers: &mut Vec<Header>,
@@ -709,6 +750,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
placeholdertext = Some( placeholdertext = Some(
self.context self.context
.stock_str(StockMessage::AcSetupMsgBody) .stock_str(StockMessage::AcSetupMsgBody)
.await
.to_string(), .to_string(),
); );
} }
@@ -755,7 +797,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
meta.viewtype = Viewtype::Image; meta.viewtype = Viewtype::Image;
meta.param.set(Param::File, grpimage); meta.param.set(Param::File, grpimage);
let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image")?; let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image").await?;
meta_part = Some(mail); 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));
} }
@@ -826,13 +868,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
// add attachment part // add attachment part
if chat::msgtype_has_file(self.msg.viewtype) { if chat::msgtype_has_file(self.msg.viewtype) {
if !is_file_size_okay(context, &self.msg) { if !is_file_size_okay(context, &self.msg).await {
bail!( bail!(
"Message exceeds the recommended {} MB.", "Message exceeds the recommended {} MB.",
RECOMMENDED_FILE_SIZE / 1_000_000, RECOMMENDED_FILE_SIZE / 1_000_000,
); );
} else { } else {
let (file_part, _) = build_body_file(context, &self.msg, "")?; let (file_part, _) = build_body_file(context, &self.msg, "").await?;
parts.push(file_part); parts.push(file_part);
} }
} }
@@ -845,8 +887,8 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
parts.push(msg_kml_part); parts.push(msg_kml_part);
} }
if location::is_sending_locations_to_chat(context, self.msg.chat_id) { if location::is_sending_locations_to_chat(context, self.msg.chat_id).await {
match self.get_location_kml_part() { match self.get_location_kml_part().await {
Ok(part) => parts.push(part), Ok(part) => parts.push(part),
Err(err) => { Err(err) => {
warn!(context, "mimefactory: could not send location: {}", err); warn!(context, "mimefactory: could not send location: {}", err);
@@ -855,7 +897,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
} }
if self.attach_selfavatar { if self.attach_selfavatar {
match context.get_config(Config::Selfavatar) { match context.get_config(Config::Selfavatar).await {
Some(path) => match build_selfavatar_file(context, &path) { Some(path) => match build_selfavatar_file(context, &path) {
Ok((part, filename)) => { Ok((part, filename)) => {
parts.push(part); parts.push(part);
@@ -882,7 +924,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
} }
/// Render an MDN /// Render an MDN
fn render_mdn(&mut self) -> Result<PartBuilder, Error> { async fn render_mdn(&mut self) -> Result<PartBuilder, Error> {
// RFC 6522, this also requires the `report-type` parameter which is equal // RFC 6522, this also requires the `report-type` parameter which is equal
// to the MIME subtype of the second body part of the multipart/report // to the MIME subtype of the second body part of the multipart/report
// //
@@ -917,13 +959,15 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
{ {
self.context self.context
.stock_str(StockMessage::EncryptedMsg) .stock_str(StockMessage::EncryptedMsg)
.await
.into_owned() .into_owned()
} else { } else {
self.msg.get_summarytext(self.context, 32) self.msg.get_summarytext(self.context, 32).await
}; };
let p2 = self let p2 = self
.context .context
.stock_string_repl_str(StockMessage::ReadRcptMailBody, p1); .stock_string_repl_str(StockMessage::ReadRcptMailBody, p1)
.await;
let message_text = format!("{}\r\n", p2); let message_text = format!("{}\r\n", p2);
message = message.child( message = message.child(
PartBuilder::new() PartBuilder::new()
@@ -980,14 +1024,15 @@ fn wrapped_base64_encode(buf: &[u8]) -> String {
.join("\r\n") .join("\r\n")
} }
fn build_body_file( async fn build_body_file(
context: &Context, context: &Context,
msg: &Message, msg: &Message,
base_name: &str, base_name: &str,
) -> Result<(PartBuilder, String), Error> { ) -> Result<(PartBuilder, String), Error> {
let blob = msg let blob = msg
.param .param
.get_blob(Param::File, context, true)? .get_blob(Param::File, context, true)
.await?
.ok_or_else(|| format_err!("msg has no filename"))?; .ok_or_else(|| format_err!("msg has no filename"))?;
let suffix = blob.suffix().unwrap_or("dat"); let suffix = blob.suffix().unwrap_or("dat");
@@ -1083,10 +1128,10 @@ fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool
.any(|(_, cur)| cur.to_lowercase() == addr_lc) .any(|(_, cur)| cur.to_lowercase() == addr_lc)
} }
fn is_file_size_okay(context: &Context, msg: &Message) -> bool { async fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
match msg.param.get_path(Param::File, context).unwrap_or(None) { match msg.param.get_path(Param::File, context).unwrap_or(None) {
Some(path) => { Some(path) => {
let bytes = dc_get_filebytes(context, &path); let bytes = dc_get_filebytes(context, &path).await;
bytes <= UPPER_LIMIT_FILE_SIZE bytes <= UPPER_LIMIT_FILE_SIZE
} }
None => false, None => false,

View File

@@ -1,4 +1,6 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use deltachat_derive::{FromSql, ToSql}; use deltachat_derive::{FromSql, ToSql};
use lettre_email::mime::{self, Mime}; use lettre_email::mime::{self, Mime};
@@ -82,7 +84,7 @@ impl Default for SystemMessage {
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup"; const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
impl MimeMessage { impl MimeMessage {
pub fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> { pub async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
let mail = mailparse::parse_mail(body)?; let mail = mailparse::parse_mail(body)?;
let message_time = mail let message_time = mail
@@ -113,7 +115,7 @@ impl MimeMessage {
let mail_raw; let mail_raw;
let mut gossipped_addr = Default::default(); let mut gossipped_addr = Default::default();
let (mail, signatures) = match e2ee::try_decrypt(context, &mail, message_time) { let (mail, signatures) = match e2ee::try_decrypt(context, &mail, message_time).await {
Ok((raw, signatures)) => { Ok((raw, signatures)) => {
if let Some(raw) = raw { if let Some(raw) = raw {
// Valid autocrypt message, encrypted // Valid autocrypt message, encrypted
@@ -128,7 +130,8 @@ impl MimeMessage {
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf // "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
let gossip_headers = decrypted_mail.headers.get_all_values("Autocrypt-Gossip"); let gossip_headers = decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossipped_addr = gossipped_addr =
update_gossip_peerstates(context, message_time, &mail, gossip_headers)?; update_gossip_peerstates(context, message_time, &mail, gossip_headers)
.await?;
// let known protected headers from the decrypted // let known protected headers from the decrypted
// part override the unencrypted top-level // part override the unencrypted top-level
@@ -179,7 +182,7 @@ impl MimeMessage {
user_avatar: None, user_avatar: None,
group_avatar: None, group_avatar: None,
}; };
parser.parse_mime_recursive(context, &mail)?; parser.parse_mime_recursive(context, &mail).await?;
parser.parse_headers(context)?; parser.parse_headers(context)?;
Ok(parser) Ok(parser)
@@ -410,63 +413,69 @@ impl MimeMessage {
self.header.get(headerdef.get_headername()) self.header.get(headerdef.get_headername())
} }
fn parse_mime_recursive( fn parse_mime_recursive<'a>(
&mut self, &'a mut self,
context: &Context, context: &'a Context,
mail: &mailparse::ParsedMail<'_>, mail: &'a mailparse::ParsedMail<'a>,
) -> Result<bool> { ) -> Pin<Box<dyn Future<Output = Result<bool>> + 'a + Send>> {
if mail.ctype.params.get("protected-headers").is_some() { use futures::future::FutureExt;
if mail.ctype.mimetype == "text/rfc822-headers" {
warn!(
context,
"Protected headers found in text/rfc822-headers attachment: Will be ignored.",
);
return Ok(false);
}
warn!(context, "Ignoring nested protected headers"); // Boxed future to deal with recursion
} async move {
if mail.ctype.params.get("protected-headers").is_some() {
enum MimeS { if mail.ctype.mimetype == "text/rfc822-headers" {
Multiple, warn!(
Single, context,
Message, "Protected headers found in text/rfc822-headers attachment: Will be ignored.",
} );
let mimetype = mail.ctype.mimetype.to_lowercase();
let m = if mimetype.starts_with("multipart") {
if mail.ctype.params.get("boundary").is_some() {
MimeS::Multiple
} else {
MimeS::Single
}
} else if mimetype.starts_with("message") {
if mimetype == "message/rfc822" {
MimeS::Message
} else {
MimeS::Single
}
} else {
MimeS::Single
};
match m {
MimeS::Multiple => self.handle_multiple(context, mail),
MimeS::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(false); return Ok(false);
} }
let mail = mailparse::parse_mail(&raw).unwrap();
self.parse_mime_recursive(context, &mail) warn!(context, "Ignoring nested protected headers");
}
enum MimeS {
Multiple,
Single,
Message,
}
let mimetype = mail.ctype.mimetype.to_lowercase();
let m = if mimetype.starts_with("multipart") {
if mail.ctype.params.get("boundary").is_some() {
MimeS::Multiple
} else {
MimeS::Single
}
} else if mimetype.starts_with("message") {
if mimetype == "message/rfc822" {
MimeS::Message
} else {
MimeS::Single
}
} else {
MimeS::Single
};
match m {
MimeS::Multiple => self.handle_multiple(context, mail).await,
MimeS::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(false);
}
let mail = mailparse::parse_mail(&raw).unwrap();
self.parse_mime_recursive(context, &mail).await
}
MimeS::Single => self.add_single_part_if_known(context, mail).await,
} }
MimeS::Single => self.add_single_part_if_known(context, mail),
} }
.boxed()
} }
fn handle_multiple( async fn handle_multiple(
&mut self, &mut self,
context: &Context, context: &Context,
mail: &mailparse::ParsedMail<'_>, mail: &mailparse::ParsedMail<'_>,
@@ -483,7 +492,7 @@ impl MimeMessage {
if get_mime_type(cur_data)?.0 == "multipart/mixed" if get_mime_type(cur_data)?.0 == "multipart/mixed"
|| get_mime_type(cur_data)?.0 == "multipart/related" || get_mime_type(cur_data)?.0 == "multipart/related"
{ {
any_part_added = self.parse_mime_recursive(context, cur_data)?; any_part_added = self.parse_mime_recursive(context, cur_data).await?;
break; break;
} }
} }
@@ -491,7 +500,7 @@ impl MimeMessage {
/* search for text/plain and add this */ /* search for text/plain and add this */
for cur_data in &mail.subparts { for cur_data in &mail.subparts {
if get_mime_type(cur_data)?.0.type_() == mime::TEXT { 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(context, cur_data).await?;
break; break;
} }
} }
@@ -499,7 +508,7 @@ impl MimeMessage {
if !any_part_added { if !any_part_added {
/* `text/plain` not found - use the first part */ /* `text/plain` not found - use the first part */
for cur_part in &mail.subparts { for cur_part in &mail.subparts {
if self.parse_mime_recursive(context, cur_part)? { if self.parse_mime_recursive(context, cur_part).await? {
any_part_added = true; any_part_added = true;
break; break;
} }
@@ -510,7 +519,7 @@ impl MimeMessage {
// we currently do not try to decrypt non-autocrypt messages // we currently do not try to decrypt non-autocrypt messages
// at all. If we see an encrypted part, we set // at all. If we see an encrypted part, we set
// decrypting_failed. // decrypting_failed.
let msg_body = context.stock_str(StockMessage::CantDecryptMsgBody); let msg_body = context.stock_str(StockMessage::CantDecryptMsgBody).await;
let txt = format!("[{}]", msg_body); let txt = format!("[{}]", msg_body);
let mut part = Part::default(); let mut part = Part::default();
@@ -533,7 +542,7 @@ impl MimeMessage {
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
for background information why we use encrypted+signed) */ for background information why we use encrypted+signed) */
if let Some(first) = mail.subparts.iter().next() { if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first)?; any_part_added = self.parse_mime_recursive(context, first).await?;
} }
} }
(mime::MULTIPART, "report") => { (mime::MULTIPART, "report") => {
@@ -558,7 +567,7 @@ impl MimeMessage {
/* eg. `report-type=delivery-status`; /* eg. `report-type=delivery-status`;
maybe we should show them as a little error icon */ maybe we should show them as a little error icon */
if let Some(first) = mail.subparts.iter().next() { if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first)?; any_part_added = self.parse_mime_recursive(context, first).await?;
} }
} }
} }
@@ -568,7 +577,7 @@ impl MimeMessage {
// Add all parts (in fact, AddSinglePartIfKnown() later check if // Add all parts (in fact, AddSinglePartIfKnown() later check if
// the parts are really supported) // the parts are really supported)
for cur_data in mail.subparts.iter() { for cur_data in mail.subparts.iter() {
if self.parse_mime_recursive(context, cur_data)? { if self.parse_mime_recursive(context, cur_data).await? {
any_part_added = true; any_part_added = true;
} }
} }
@@ -578,7 +587,7 @@ impl MimeMessage {
Ok(any_part_added) Ok(any_part_added)
} }
fn add_single_part_if_known( async fn add_single_part_if_known(
&mut self, &mut self,
context: &Context, context: &Context,
mail: &mailparse::ParsedMail<'_>, mail: &mailparse::ParsedMail<'_>,
@@ -600,7 +609,8 @@ impl MimeMessage {
&raw_mime, &raw_mime,
&mail.get_body_raw()?, &mail.get_body_raw()?,
&filename, &filename,
); )
.await;
} }
None => { None => {
match mime_type.type_() { match mime_type.type_() {
@@ -652,7 +662,7 @@ impl MimeMessage {
Ok(self.parts.len() > old_part_count) Ok(self.parts.len() > old_part_count)
} }
fn do_add_single_file_part( async fn do_add_single_file_part(
&mut self, &mut self,
context: &Context, context: &Context,
msg_type: Viewtype, msg_type: Viewtype,
@@ -685,7 +695,7 @@ impl MimeMessage {
/* we have a regular file attachment, /* we have a regular file attachment,
write decoded data to new blob object */ write decoded data to new blob object */
let blob = match BlobObject::create(context, filename, decoded_data) { let blob = match BlobObject::create(context, filename, decoded_data).await {
Ok(blob) => blob, Ok(blob) => blob,
Err(err) => { Err(err) => {
error!( error!(
@@ -829,7 +839,7 @@ impl MimeMessage {
} }
/// Handle reports (only MDNs for now) /// Handle reports (only MDNs for now)
pub fn handle_reports(&self, context: &Context, from_id: u32, sent_timestamp: i64) { pub async fn handle_reports(&self, context: &Context, from_id: u32, sent_timestamp: i64) {
if self.reports.is_empty() { if self.reports.is_empty() {
return; return;
} }
@@ -840,15 +850,16 @@ impl MimeMessage {
{ {
if let Some((chat_id, msg_id)) = if let Some((chat_id, msg_id)) =
message::mdn_from_ext(context, from_id, original_message_id, sent_timestamp) message::mdn_from_ext(context, from_id, original_message_id, sent_timestamp)
.await
{ {
context.call_cb(Event::MsgRead { chat_id, msg_id }); context.emit_event(Event::MsgRead { chat_id, msg_id });
} }
} }
} }
} }
} }
fn update_gossip_peerstates( async fn update_gossip_peerstates(
context: &Context, context: &Context,
message_time: i64, message_time: i64,
mail: &mailparse::ParsedMail<'_>, mail: &mailparse::ParsedMail<'_>,
@@ -865,18 +876,18 @@ fn update_gossip_peerstates(
.iter() .iter()
.any(|info| info.addr == header.addr.to_lowercase()) .any(|info| info.addr == header.addr.to_lowercase())
{ {
let mut peerstate = Peerstate::from_addr(context, &context.sql, &header.addr); let mut peerstate = Peerstate::from_addr(context, &header.addr).await;
if let Some(ref mut peerstate) = peerstate { if let Some(ref mut peerstate) = peerstate {
peerstate.apply_gossip(header, message_time); peerstate.apply_gossip(header, message_time);
peerstate.save_to_db(&context.sql, false)?; peerstate.save_to_db(&context.sql, false).await?;
} else { } else {
let p = Peerstate::from_gossip(context, header, message_time); let p = Peerstate::from_gossip(context, header, message_time);
p.save_to_db(&context.sql, true)?; p.save_to_db(&context.sql, true).await?;
peerstate = Some(p); peerstate = Some(p);
} }
if let Some(peerstate) = peerstate { if let Some(peerstate) = peerstate {
if peerstate.degrade_event.is_some() { if peerstate.degrade_event.is_some() {
handle_degrade_event(context, &peerstate)?; handle_degrade_event(context, &peerstate).await?;
} }
} }
@@ -1104,21 +1115,25 @@ mod tests {
} }
} }
#[test] #[async_std::test]
fn test_dc_mimeparser_crash() { async fn test_dc_mimeparser_crash() {
let context = dummy_context(); let context = dummy_context().await;
let raw = include_bytes!("../test-data/message/issue_523.txt"); let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(mimeparser.get_subject(), None); assert_eq!(mimeparser.get_subject(), None);
assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts.len(), 1);
} }
#[test] #[async_std::test]
fn test_get_rfc724_mid_exists() { async fn test_get_rfc724_mid_exists() {
let context = dummy_context(); let context = dummy_context().await;
let raw = include_bytes!("../test-data/message/mail_with_message_id.txt"); let raw = include_bytes!("../test-data/message/mail_with_message_id.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!( assert_eq!(
mimeparser.get_rfc724_mid(), mimeparser.get_rfc724_mid(),
@@ -1126,11 +1141,13 @@ mod tests {
); );
} }
#[test] #[async_std::test]
fn test_get_rfc724_mid_not_exists() { async fn test_get_rfc724_mid_not_exists() {
let context = dummy_context(); let context = dummy_context().await;
let raw = include_bytes!("../test-data/message/issue_523.txt"); let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(mimeparser.get_rfc724_mid(), None); assert_eq!(mimeparser.get_rfc724_mid(), None);
} }
@@ -1182,9 +1199,9 @@ mod tests {
); );
} }
#[test] #[async_std::test]
fn test_parse_first_addr() { async fn test_parse_first_addr() {
let context = dummy_context(); let context = dummy_context().await;
let raw = b"From: hello@one.org, world@two.org\n\ let raw = b"From: hello@one.org, world@two.org\n\
Chat-Disposition-Notification-To: wrong\n\ Chat-Disposition-Notification-To: wrong\n\
Content-Type: text/plain\n\ Content-Type: text/plain\n\
@@ -1193,7 +1210,9 @@ mod tests {
test1\n\ test1\n\
"; ";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
let of = &mimeparser.from[0]; let of = &mimeparser.from[0];
assert_eq!(of.addr, "hello@one.org"); assert_eq!(of.addr, "hello@one.org");
@@ -1201,9 +1220,9 @@ mod tests {
assert!(mimeparser.chat_disposition_notification_to.is_none()); assert!(mimeparser.chat_disposition_notification_to.is_none());
} }
#[test] #[async_std::test]
fn test_mimeparser_with_context() { async fn test_mimeparser_with_context() {
let context = dummy_context(); let context = dummy_context().await;
let raw = b"From: hello\n\ let raw = b"From: hello\n\
Content-Type: multipart/mixed; boundary=\"==break==\";\n\ Content-Type: multipart/mixed; boundary=\"==break==\";\n\
Subject: outer-subject\n\ Subject: outer-subject\n\
@@ -1224,7 +1243,9 @@ mod tests {
--==break==--\n\ --==break==--\n\
\n"; \n";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
// non-overwritten headers do not bubble up // non-overwritten headers do not bubble up
let of = mimeparser.get(HeaderDef::SecureJoinGroup).unwrap(); let of = mimeparser.get(HeaderDef::SecureJoinGroup).unwrap();
@@ -1249,31 +1270,31 @@ mod tests {
assert!(mimeparser.get(HeaderDef::SecureJoinFingerprint).is_none()); assert!(mimeparser.get(HeaderDef::SecureJoinFingerprint).is_none());
} }
#[test] #[async_std::test]
fn test_mimeparser_with_avatars() { async fn test_mimeparser_with_avatars() {
let t = dummy_context(); let t = dummy_context().await;
let raw = include_bytes!("../test-data/message/mail_attach_txt.eml"); let raw = include_bytes!("../test-data/message/mail_attach_txt.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap(); let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
assert_eq!(mimeparser.user_avatar, None); assert_eq!(mimeparser.user_avatar, None);
assert_eq!(mimeparser.group_avatar, None); assert_eq!(mimeparser.group_avatar, None);
let raw = include_bytes!("../test-data/message/mail_with_user_avatar.eml"); let raw = include_bytes!("../test-data/message/mail_with_user_avatar.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap(); let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert!(mimeparser.user_avatar.unwrap().is_change()); assert!(mimeparser.user_avatar.unwrap().is_change());
assert_eq!(mimeparser.group_avatar, None); assert_eq!(mimeparser.group_avatar, None);
let raw = include_bytes!("../test-data/message/mail_with_user_avatar_deleted.eml"); let raw = include_bytes!("../test-data/message/mail_with_user_avatar_deleted.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap(); let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert_eq!(mimeparser.user_avatar, Some(AvatarAction::Delete)); assert_eq!(mimeparser.user_avatar, Some(AvatarAction::Delete));
assert_eq!(mimeparser.group_avatar, None); assert_eq!(mimeparser.group_avatar, None);
let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml"); let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).unwrap(); let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text); assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert!(mimeparser.user_avatar.unwrap().is_change()); assert!(mimeparser.user_avatar.unwrap().is_change());
@@ -1283,16 +1304,18 @@ mod tests {
let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml"); let raw = include_bytes!("../test-data/message/mail_with_user_and_group_avatars.eml");
let raw = String::from_utf8_lossy(raw).to_string(); let raw = String::from_utf8_lossy(raw).to_string();
let raw = raw.replace("Chat-User-Avatar:", "Xhat-Xser-Xvatar:"); let raw = raw.replace("Chat-User-Avatar:", "Xhat-Xser-Xvatar:");
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw.as_bytes()).unwrap(); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw.as_bytes())
.await
.unwrap();
assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Image); assert_eq!(mimeparser.parts[0].typ, Viewtype::Image);
assert_eq!(mimeparser.user_avatar, None); assert_eq!(mimeparser.user_avatar, None);
assert!(mimeparser.group_avatar.unwrap().is_change()); assert!(mimeparser.group_avatar.unwrap().is_change());
} }
#[test] #[async_std::test]
fn test_mimeparser_message_kml() { async fn test_mimeparser_message_kml() {
let context = dummy_context(); let context = dummy_context().await;
let raw = b"Chat-Version: 1.0\n\ let raw = b"Chat-Version: 1.0\n\
From: foo <foo@example.org>\n\ From: foo <foo@example.org>\n\
To: bar <bar@example.org>\n\ To: bar <bar@example.org>\n\
@@ -1320,7 +1343,9 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\
--==break==--\n\ --==break==--\n\
;"; ;";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!( assert_eq!(
mimeparser.get_subject(), mimeparser.get_subject(),
Some("Location streaming".to_string()) Some("Location streaming".to_string())
@@ -1333,9 +1358,9 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\
assert_eq!(mimeparser.parts.len(), 1); assert_eq!(mimeparser.parts.len(), 1);
} }
#[test] #[async_std::test]
fn test_parse_mdn() { async fn test_parse_mdn() {
let context = dummy_context(); let context = dummy_context().await;
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\ Chat-Version: 1.0\n\
@@ -1367,7 +1392,9 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ --kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
"; ";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!( assert_eq!(
message.get_subject(), message.get_subject(),
Some("Chat: Message opened".to_string()) Some("Chat: Message opened".to_string())
@@ -1381,9 +1408,9 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
/// ///
/// RFC 6522 specifically allows MDNs to be nested inside /// RFC 6522 specifically allows MDNs to be nested inside
/// multipart MIME messages. /// multipart MIME messages.
#[test] #[async_std::test]
fn test_parse_multiple_mdns() { async fn test_parse_multiple_mdns() {
let context = dummy_context(); let context = dummy_context().await;
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\ Chat-Version: 1.0\n\
@@ -1445,7 +1472,9 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
--outer--\n\ --outer--\n\
"; ";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!( assert_eq!(
message.get_subject(), message.get_subject(),
Some("Chat: Message opened".to_string()) Some("Chat: Message opened".to_string())
@@ -1455,9 +1484,9 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
assert_eq!(message.reports.len(), 2); assert_eq!(message.reports.len(), 2);
} }
#[test] #[async_std::test]
fn test_parse_mdn_with_additional_message_ids() { async fn test_parse_mdn_with_additional_message_ids() {
let context = dummy_context(); let context = dummy_context().await;
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\ Chat-Version: 1.0\n\
@@ -1490,7 +1519,9 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\ --kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
"; ";
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!( assert_eq!(
message.get_subject(), message.get_subject(),
Some("Chat: Message opened".to_string()) Some("Chat: Message opened".to_string())
@@ -1505,9 +1536,9 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
); );
} }
#[test] #[async_std::test]
fn test_parse_inline_attachment() { async fn test_parse_inline_attachment() {
let context = dummy_context(); let context = dummy_context().await;
let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC) let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC)
From: sender@example.com From: sender@example.com
To: receiver@example.com To: receiver@example.com
@@ -1532,7 +1563,9 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
------=_Part_25_46172632.1581201680436-- ------=_Part_25_46172632.1581201680436--
"#; "#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!( assert_eq!(
message.get_subject(), message.get_subject(),
Some("Mail with inline attachment".to_string()) Some("Mail with inline attachment".to_string())
@@ -1543,9 +1576,9 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
assert_eq!(message.parts[0].msg, "Hello!"); assert_eq!(message.parts[0].msg, "Hello!");
} }
#[test] #[async_std::test]
fn parse_inline_image() { async fn parse_inline_image() {
let context = dummy_context(); let context = dummy_context().await;
let raw = br#"Message-ID: <foobar@example.org> let raw = br#"Message-ID: <foobar@example.org>
From: foo <foo@example.org> From: foo <foo@example.org>
Subject: example Subject: example
@@ -1579,7 +1612,9 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
----11019878869865180-- ----11019878869865180--
"#; "#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(message.get_subject(), Some("example".to_string())); assert_eq!(message.get_subject(), Some("example".to_string()));
assert_eq!(message.parts.len(), 1); assert_eq!(message.parts.len(), 1);
@@ -1587,9 +1622,9 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
assert_eq!(message.parts[0].msg, "Test"); assert_eq!(message.parts[0].msg, "Test");
} }
#[test] #[async_std::test]
fn parse_thunderbird_html_embedded_image() { async fn parse_thunderbird_html_embedded_image() {
let context = dummy_context(); let context = dummy_context().await;
let raw = br#"To: Alice <alice@example.org> let raw = br#"To: Alice <alice@example.org>
From: Bob <bob@example.org> From: Bob <bob@example.org>
Subject: Test subject Subject: Test subject
@@ -1649,7 +1684,9 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
--------------779C1631600DF3DB8C02E53A--"#; --------------779C1631600DF3DB8C02E53A--"#;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(message.get_subject(), Some("Test subject".to_string())); assert_eq!(message.get_subject(), Some("Test subject".to_string()));
assert_eq!(message.parts.len(), 1); assert_eq!(message.parts.len(), 1);
@@ -1658,9 +1695,9 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
} }
// Outlook specifies filename in the "name" attribute of Content-Type // Outlook specifies filename in the "name" attribute of Content-Type
#[test] #[async_std::test]
fn parse_outlook_html_embedded_image() { async fn parse_outlook_html_embedded_image() {
let context = dummy_context(); let context = dummy_context().await;
let raw = br##"From: Anonymous <anonymous@example.org> let raw = br##"From: Anonymous <anonymous@example.org>
To: Anonymous <anonymous@example.org> To: Anonymous <anonymous@example.org>
Subject: Delta Chat is great stuff! Subject: Delta Chat is great stuff!
@@ -1718,7 +1755,9 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
------=_NextPart_000_0003_01D622B3.CA753E60-- ------=_NextPart_000_0003_01D622B3.CA753E60--
"##; "##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap(); let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!( assert_eq!(
message.get_subject(), message.get_subject(),
Some("Delta Chat is great stuff!".to_string()) Some("Delta Chat is great stuff!".to_string())

View File

@@ -48,7 +48,7 @@ struct Response {
scope: Option<String>, scope: Option<String>,
} }
pub fn dc_get_oauth2_url( pub async fn dc_get_oauth2_url(
context: &Context, context: &Context,
addr: impl AsRef<str>, addr: impl AsRef<str>,
redirect_uri: impl AsRef<str>, redirect_uri: impl AsRef<str>,
@@ -61,6 +61,7 @@ pub fn dc_get_oauth2_url(
"oauth2_pending_redirect_uri", "oauth2_pending_redirect_uri",
Some(redirect_uri.as_ref()), Some(redirect_uri.as_ref()),
) )
.await
.is_err() .is_err()
{ {
return None; return None;
@@ -74,21 +75,21 @@ pub fn dc_get_oauth2_url(
} }
} }
// The following function may block due http-requests; pub async fn dc_get_oauth2_access_token(
// must not be called from the main thread or by the ui!
pub fn dc_get_oauth2_access_token(
context: &Context, context: &Context,
addr: impl AsRef<str>, addr: impl AsRef<str>,
code: impl AsRef<str>, code: impl AsRef<str>,
regenerate: bool, regenerate: bool,
) -> Option<String> { ) -> Option<String> {
if let Some(oauth2) = Oauth2::from_address(addr) { if let Some(oauth2) = Oauth2::from_address(addr) {
let lock = context.oauth2_critical.clone(); let lock = context.oauth2_mutex.lock().await;
let _l = lock.lock().unwrap();
// read generated token // read generated token
if !regenerate && !is_expired(context) { if !regenerate && !is_expired(context).await {
let access_token = context.sql.get_raw_config(context, "oauth2_access_token"); let access_token = context
.sql
.get_raw_config(context, "oauth2_access_token")
.await;
if access_token.is_some() { if access_token.is_some() {
// success // success
return access_token; return access_token;
@@ -96,10 +97,14 @@ pub fn dc_get_oauth2_access_token(
} }
// generate new token: build & call auth url // generate new token: build & call auth url
let refresh_token = context.sql.get_raw_config(context, "oauth2_refresh_token"); let refresh_token = context
.sql
.get_raw_config(context, "oauth2_refresh_token")
.await;
let refresh_token_for = context let refresh_token_for = context
.sql .sql
.get_raw_config(context, "oauth2_refresh_token_for") .get_raw_config(context, "oauth2_refresh_token_for")
.await
.unwrap_or_else(|| "unset".into()); .unwrap_or_else(|| "unset".into());
let (redirect_uri, token_url, update_redirect_uri_on_success) = let (redirect_uri, token_url, update_redirect_uri_on_success) =
@@ -109,6 +114,7 @@ pub fn dc_get_oauth2_access_token(
context context
.sql .sql
.get_raw_config(context, "oauth2_pending_redirect_uri") .get_raw_config(context, "oauth2_pending_redirect_uri")
.await
.unwrap_or_else(|| "unset".into()), .unwrap_or_else(|| "unset".into()),
oauth2.init_token, oauth2.init_token,
true, true,
@@ -122,6 +128,7 @@ pub fn dc_get_oauth2_access_token(
context context
.sql .sql
.get_raw_config(context, "oauth2_redirect_uri") .get_raw_config(context, "oauth2_redirect_uri")
.await
.unwrap_or_else(|| "unset".into()), .unwrap_or_else(|| "unset".into()),
oauth2.refresh_token, oauth2.refresh_token,
false, false,
@@ -154,10 +161,7 @@ pub fn dc_get_oauth2_access_token(
} }
// ... and POST // ... and POST
let response = reqwest::blocking::Client::new() let response = surf::post(post_url).body_form(&post_param);
.post(post_url)
.form(&post_param)
.send();
if response.is_err() { if response.is_err() {
warn!( warn!(
context, context,
@@ -165,19 +169,8 @@ pub fn dc_get_oauth2_access_token(
); );
return None; return None;
} }
let response = response.unwrap();
if !response.status().is_success() {
warn!(
context,
"Unsuccessful response when calling OAuth2 at {}: {:?}",
token_url,
response.status()
);
return None;
}
// generate new token: parse returned json let parsed: Result<Response, _> = response.unwrap().recv_json().await;
let parsed: reqwest::Result<Response> = response.json();
if parsed.is_err() { if parsed.is_err() {
warn!( warn!(
context, context,
@@ -185,7 +178,6 @@ pub fn dc_get_oauth2_access_token(
); );
return None; return None;
} }
println!("response: {:?}", &parsed);
// update refresh_token if given, typically on the first round, but we update it later as well. // update refresh_token if given, typically on the first round, but we update it later as well.
let response = parsed.unwrap(); let response = parsed.unwrap();
@@ -193,10 +185,12 @@ pub fn dc_get_oauth2_access_token(
context context
.sql .sql
.set_raw_config(context, "oauth2_refresh_token", Some(token)) .set_raw_config(context, "oauth2_refresh_token", Some(token))
.await
.ok(); .ok();
context context
.sql .sql
.set_raw_config(context, "oauth2_refresh_token_for", Some(code.as_ref())) .set_raw_config(context, "oauth2_refresh_token_for", Some(code.as_ref()))
.await
.ok(); .ok();
} }
@@ -206,6 +200,7 @@ pub fn dc_get_oauth2_access_token(
context context
.sql .sql
.set_raw_config(context, "oauth2_access_token", Some(token)) .set_raw_config(context, "oauth2_access_token", Some(token))
.await
.ok(); .ok();
let expires_in = response let expires_in = response
.expires_in .expires_in
@@ -215,18 +210,22 @@ pub fn dc_get_oauth2_access_token(
context context
.sql .sql
.set_raw_config_int64(context, "oauth2_timestamp_expires", expires_in) .set_raw_config_int64(context, "oauth2_timestamp_expires", expires_in)
.await
.ok(); .ok();
if update_redirect_uri_on_success { if update_redirect_uri_on_success {
context context
.sql .sql
.set_raw_config(context, "oauth2_redirect_uri", Some(redirect_uri.as_ref())) .set_raw_config(context, "oauth2_redirect_uri", Some(redirect_uri.as_ref()))
.await
.ok(); .ok();
} }
} else { } else {
warn!(context, "Failed to find OAuth2 access token"); warn!(context, "Failed to find OAuth2 access token");
} }
drop(lock);
response.access_token response.access_token
} else { } else {
warn!(context, "Internal OAuth2 error: 2"); warn!(context, "Internal OAuth2 error: 2");
@@ -235,7 +234,7 @@ pub fn dc_get_oauth2_access_token(
} }
} }
pub fn dc_get_oauth2_addr( pub async fn dc_get_oauth2_addr(
context: &Context, context: &Context,
addr: impl AsRef<str>, addr: impl AsRef<str>,
code: impl AsRef<str>, code: impl AsRef<str>,
@@ -244,13 +243,14 @@ pub fn dc_get_oauth2_addr(
oauth2.get_userinfo?; oauth2.get_userinfo?;
if let Some(access_token) = if let Some(access_token) =
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false) dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await
{ {
let addr_out = oauth2.get_addr(context, access_token); let addr_out = oauth2.get_addr(context, access_token).await;
if addr_out.is_none() { if addr_out.is_none() {
// regenerate // regenerate
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true) { if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true).await
oauth2.get_addr(context, access_token) {
oauth2.get_addr(context, access_token).await
} else { } else {
None None
} }
@@ -280,7 +280,7 @@ impl Oauth2 {
} }
} }
fn get_addr(&self, context: &Context, access_token: impl AsRef<str>) -> Option<String> { async fn get_addr(&self, context: &Context, access_token: impl AsRef<str>) -> Option<String> {
let userinfo_url = self.get_userinfo.unwrap_or_else(|| ""); let userinfo_url = self.get_userinfo.unwrap_or_else(|| "");
let userinfo_url = replace_in_uri(&userinfo_url, "$ACCESS_TOKEN", access_token); let userinfo_url = replace_in_uri(&userinfo_url, "$ACCESS_TOKEN", access_token);
@@ -291,50 +291,35 @@ impl Oauth2 {
// "verified_email": true, // "verified_email": true,
// "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg" // "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg"
// } // }
let response = reqwest::blocking::Client::new().get(&userinfo_url).send(); let response: Result<HashMap<String, serde_json::Value>, surf::Error> =
surf::get(userinfo_url).recv_json().await;
if response.is_err() { if response.is_err() {
warn!(context, "Error getting userinfo: {:?}", response); warn!(context, "Error getting userinfo: {:?}", response);
return None; return None;
} }
let response = response.unwrap();
if !response.status().is_success() {
warn!(context, "Error getting userinfo: {:?}", response.status());
return None;
}
let parsed: reqwest::Result<HashMap<String, serde_json::Value>> = response.json(); let parsed = response.unwrap();
if parsed.is_err() { // CAVE: serde_json::Value.as_str() removes the quotes of json-strings
warn!( // but serde_json::Value.to_string() does not!
context, if let Some(addr) = parsed.get("email") {
"Failed to parse userinfo JSON response: {:?}", parsed if let Some(s) = addr.as_str() {
); Some(s.to_string())
return None;
}
if let Ok(response) = parsed {
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
// but serde_json::Value.to_string() does not!
if let Some(addr) = response.get("email") {
if let Some(s) = addr.as_str() {
Some(s.to_string())
} else {
warn!(context, "E-mail in userinfo is not a string: {}", addr);
None
}
} else { } else {
warn!(context, "E-mail missing in userinfo."); warn!(context, "E-mail in userinfo is not a string: {}", addr);
None None
} }
} else { } else {
warn!(context, "Failed to parse userinfo."); warn!(context, "E-mail missing in userinfo.");
None None
} }
} }
} }
fn is_expired(context: &Context) -> bool { async fn is_expired(context: &Context) -> bool {
let expire_timestamp = context let expire_timestamp = context
.sql .sql
.get_raw_config_int64(context, "oauth2_timestamp_expires") .get_raw_config_int64(context, "oauth2_timestamp_expires")
.await
.unwrap_or_default(); .unwrap_or_default();
if expire_timestamp <= 0 { if expire_timestamp <= 0 {
@@ -393,32 +378,32 @@ mod tests {
assert_eq!(Oauth2::from_address("hello@web.de"), None); assert_eq!(Oauth2::from_address("hello@web.de"), None);
} }
#[test] #[async_std::test]
fn test_dc_get_oauth2_addr() { async fn test_dc_get_oauth2_addr() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let addr = "dignifiedquire@gmail.com"; let addr = "dignifiedquire@gmail.com";
let code = "fail"; let code = "fail";
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code); let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await;
// this should fail as it is an invalid password // this should fail as it is an invalid password
assert_eq!(res, None); assert_eq!(res, None);
} }
#[test] #[async_std::test]
fn test_dc_get_oauth2_url() { async fn test_dc_get_oauth2_url() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let addr = "dignifiedquire@gmail.com"; let addr = "dignifiedquire@gmail.com";
let redirect_uri = "chat.delta:/com.b44t.messenger"; let redirect_uri = "chat.delta:/com.b44t.messenger";
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri); let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri).await;
assert_eq!(res, Some("https://accounts.google.com/o/oauth2/auth?client_id=959970109878%2D4mvtgf6feshskf7695nfln6002mom908%2Eapps%2Egoogleusercontent%2Ecom&redirect_uri=chat%2Edelta%3A%2Fcom%2Eb44t%2Emessenger&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline".into())); assert_eq!(res, Some("https://accounts.google.com/o/oauth2/auth?client_id=959970109878%2D4mvtgf6feshskf7695nfln6002mom908%2Eapps%2Egoogleusercontent%2Ecom&redirect_uri=chat%2Edelta%3A%2Fcom%2Eb44t%2Emessenger&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline".into()));
} }
#[test] #[async_std::test]
fn test_dc_get_oauth2_token() { async fn test_dc_get_oauth2_token() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let addr = "dignifiedquire@gmail.com"; let addr = "dignifiedquire@gmail.com";
let code = "fail"; let code = "fail";
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false); let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false).await;
// this should fail as it is an invalid password // this should fail as it is an invalid password
assert_eq!(res, None); assert_eq!(res, None);
} }

View File

@@ -1,8 +1,8 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt; use std::fmt;
use std::path::PathBuf;
use std::str; use std::str;
use async_std::path::PathBuf;
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -275,7 +275,8 @@ impl Params {
/// created without copying if the path already referes to a valid /// created without copying if the path already referes to a valid
/// blob. If so a [BlobObject] will be returned regardless of the /// blob. If so a [BlobObject] will be returned regardless of the
/// `create` argument. /// `create` argument.
pub fn get_blob<'a>( #[allow(clippy::needless_lifetimes)]
pub async fn get_blob<'a>(
&self, &self,
key: Param, key: Param,
context: &'a Context, context: &'a Context,
@@ -288,7 +289,7 @@ impl Params {
let file = ParamsFile::from_param(context, val)?; let file = ParamsFile::from_param(context, val)?;
let blob = match file { let blob = match file {
ParamsFile::FsPath(path) => match create { ParamsFile::FsPath(path) => match create {
true => BlobObject::new_from_path(context, path)?, true => BlobObject::new_from_path(context, path).await?,
false => BlobObject::from_path(context, path)?, false => BlobObject::from_path(context, path)?,
}, },
ParamsFile::Blob(blob) => blob, ParamsFile::Blob(blob) => blob,
@@ -362,8 +363,8 @@ impl<'a> ParamsFile<'a> {
mod tests { mod tests {
use super::*; use super::*;
use std::fs; use async_std::fs;
use std::path::Path; use async_std::path::Path;
use crate::test_utils::*; use crate::test_utils::*;
@@ -411,9 +412,9 @@ mod tests {
assert_eq!(p1.get(Param::Forwarded).unwrap(), "cli%40deltachat.de"); assert_eq!(p1.get(Param::Forwarded).unwrap(), "cli%40deltachat.de");
} }
#[test] #[async_std::test]
fn test_params_file_fs_path() { async fn test_params_file_fs_path() {
let t = dummy_context(); let t = dummy_context().await;
if let ParamsFile::FsPath(p) = ParamsFile::from_param(&t.ctx, "/foo/bar/baz").unwrap() { if let ParamsFile::FsPath(p) = ParamsFile::from_param(&t.ctx, "/foo/bar/baz").unwrap() {
assert_eq!(p, Path::new("/foo/bar/baz")); assert_eq!(p, Path::new("/foo/bar/baz"));
} else { } else {
@@ -421,9 +422,9 @@ mod tests {
} }
} }
#[test] #[async_std::test]
fn test_params_file_blob() { async fn test_params_file_blob() {
let t = dummy_context(); let t = dummy_context().await;
if let ParamsFile::Blob(b) = ParamsFile::from_param(&t.ctx, "$BLOBDIR/foo").unwrap() { if let ParamsFile::Blob(b) = ParamsFile::from_param(&t.ctx, "$BLOBDIR/foo").unwrap() {
assert_eq!(b.as_name(), "$BLOBDIR/foo"); assert_eq!(b.as_name(), "$BLOBDIR/foo");
} else { } else {
@@ -432,28 +433,33 @@ mod tests {
} }
// Tests for Params::get_file(), Params::get_path() and Params::get_blob(). // Tests for Params::get_file(), Params::get_path() and Params::get_blob().
#[test] #[async_std::test]
fn test_params_get_fileparam() { async fn test_params_get_fileparam() {
let t = dummy_context(); let t = dummy_context().await;
let fname = t.dir.path().join("foo"); let fname = t.dir.path().join("foo");
let mut p = Params::new(); let mut p = Params::new();
p.set(Param::File, fname.to_str().unwrap()); p.set(Param::File, fname.to_str().unwrap());
let file = p.get_file(Param::File, &t.ctx).unwrap().unwrap(); let file = p.get_file(Param::File, &t.ctx).unwrap().unwrap();
assert_eq!(file, ParamsFile::FsPath(fname.clone())); assert_eq!(file, ParamsFile::FsPath(fname.clone().into()));
let path = p.get_path(Param::File, &t.ctx).unwrap().unwrap(); let path: PathBuf = p.get_path(Param::File, &t.ctx).unwrap().unwrap();
let fname: PathBuf = fname.into();
assert_eq!(path, fname); assert_eq!(path, fname);
// Blob does not exist yet, expect BlobError. // Blob does not exist yet, expect BlobError.
let err = p.get_blob(Param::File, &t.ctx, false).unwrap_err(); let err = p.get_blob(Param::File, &t.ctx, false).await.unwrap_err();
match err { match err {
BlobError::WrongBlobdir { .. } => (), BlobError::WrongBlobdir { .. } => (),
_ => panic!("wrong error type/variant: {:?}", err), _ => panic!("wrong error type/variant: {:?}", err),
} }
fs::write(fname, b"boo").unwrap(); fs::write(fname, b"boo").await.unwrap();
let blob = p.get_blob(Param::File, &t.ctx, true).unwrap().unwrap(); let blob = p
.get_blob(Param::File, &t.ctx, true)
.await
.unwrap()
.unwrap();
assert_eq!( assert_eq!(
blob, blob,
BlobObject::from_name(&t.ctx, "foo".to_string()).unwrap() BlobObject::from_name(&t.ctx, "foo".to_string()).unwrap()
@@ -462,7 +468,11 @@ mod tests {
// Blob in blobdir, expect blob. // Blob in blobdir, expect blob.
let bar = t.ctx.get_blobdir().join("bar"); let bar = t.ctx.get_blobdir().join("bar");
p.set(Param::File, bar.to_str().unwrap()); p.set(Param::File, bar.to_str().unwrap());
let blob = p.get_blob(Param::File, &t.ctx, false).unwrap().unwrap(); let blob = p
.get_blob(Param::File, &t.ctx, false)
.await
.unwrap()
.unwrap();
assert_eq!( assert_eq!(
blob, blob,
BlobObject::from_name(&t.ctx, "bar".to_string()).unwrap() BlobObject::from_name(&t.ctx, "bar".to_string()).unwrap()
@@ -471,6 +481,10 @@ mod tests {
p.remove(Param::File); p.remove(Param::File);
assert!(p.get_file(Param::File, &t.ctx).unwrap().is_none()); assert!(p.get_file(Param::File, &t.ctx).unwrap().is_none());
assert!(p.get_path(Param::File, &t.ctx).unwrap().is_none()); assert!(p.get_path(Param::File, &t.ctx).unwrap().is_none());
assert!(p.get_blob(Param::File, &t.ctx, false).unwrap().is_none()); assert!(p
.get_blob(Param::File, &t.ctx, false)
.await
.unwrap()
.is_none());
} }
} }

View File

@@ -9,7 +9,7 @@ use crate::aheader::*;
use crate::constants::*; use crate::constants::*;
use crate::context::Context; use crate::context::Context;
use crate::key::{Key, SignedPublicKey}; use crate::key::{Key, SignedPublicKey};
use crate::sql::{self, Sql}; use crate::sql::Sql;
#[derive(Debug)] #[derive(Debug)]
pub enum PeerstateKeyType { pub enum PeerstateKeyType {
@@ -144,12 +144,16 @@ impl<'a> Peerstate<'a> {
res res
} }
pub fn from_addr(context: &'a Context, _sql: &Sql, addr: &str) -> Option<Self> { pub async fn from_addr(context: &'a Context, addr: &str) -> Option<Peerstate<'a>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, verified_key, verified_key_fingerprint FROM acpeerstates WHERE addr=? COLLATE NOCASE;"; let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, verified_key, verified_key_fingerprint FROM acpeerstates WHERE addr=? COLLATE NOCASE;";
Self::from_stmt(context, query, &[addr]) Self::from_stmt(context, query, paramsv![addr]).await
} }
pub fn from_fingerprint(context: &'a Context, _sql: &Sql, fingerprint: &str) -> Option<Self> { pub async fn from_fingerprint(
context: &'a Context,
_sql: &Sql,
fingerprint: &str,
) -> Option<Peerstate<'a>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \ verified_key, verified_key_fingerprint \
@@ -161,15 +165,16 @@ impl<'a> Peerstate<'a> {
Self::from_stmt( Self::from_stmt(
context, context,
query, query,
params![fingerprint, fingerprint, fingerprint], paramsv![fingerprint, fingerprint, fingerprint],
) )
.await
} }
fn from_stmt<P>(context: &'a Context, query: &str, params: P) -> Option<Self> async fn from_stmt(
where context: &'a Context,
P: IntoIterator, query: &str,
P::Item: rusqlite::ToSql, params: Vec<&dyn crate::ToSql>,
{ ) -> Option<Peerstate<'a>> {
context context
.sql .sql
.query_row(query, params, |row| { .query_row(query, params, |row| {
@@ -215,18 +220,19 @@ impl<'a> Peerstate<'a> {
res.public_key = row res.public_key = row
.get(4) .get(4)
.ok() .ok()
.and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Public)); .and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Public).ok());
res.gossip_key = row res.gossip_key = row
.get(6) .get(6)
.ok() .ok()
.and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Public)); .and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Public).ok());
res.verified_key = row res.verified_key = row
.get(9) .get(9)
.ok() .ok()
.and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Public)); .and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Public).ok());
Ok(res) Ok(res)
}) })
.await
.ok() .ok()
} }
@@ -361,6 +367,15 @@ impl<'a> Peerstate<'a> {
} }
} }
pub fn take_key(mut self, min_verified: PeerstateVerifiedStatus) -> Option<Key> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.take(),
PeerstateVerifiedStatus::Unverified => {
self.public_key.take().or_else(|| self.gossip_key.take())
}
}
}
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Key> { pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Key> {
match min_verified { match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(), PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(),
@@ -409,52 +424,48 @@ impl<'a> Peerstate<'a> {
} }
} }
pub fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> { pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> {
if create { if create {
sql::execute( sql.execute(
self.context,
sql,
"INSERT INTO acpeerstates (addr) VALUES(?);", "INSERT INTO acpeerstates (addr) VALUES(?);",
params![self.addr], paramsv![self.addr],
)?; )
.await?;
} }
if self.to_save == Some(ToSave::All) || create { if self.to_save == Some(ToSave::All) || create {
sql::execute( sql.execute(
self.context,
sql,
"UPDATE acpeerstates \ "UPDATE acpeerstates \
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \ SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \ public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \
verified_key=?, verified_key_fingerprint=? \ verified_key=?, verified_key_fingerprint=? \
WHERE addr=?;", WHERE addr=?;",
params![ paramsv![
self.last_seen, self.last_seen,
self.last_seen_autocrypt, self.last_seen_autocrypt,
self.prefer_encrypt as i64, self.prefer_encrypt as i64,
self.public_key.as_ref().map(|k| k.to_bytes()), self.public_key.as_ref().map(|k| k.to_bytes()),
self.gossip_timestamp, self.gossip_timestamp,
self.gossip_key.as_ref().map(|k| k.to_bytes()), self.gossip_key.as_ref().map(|k| k.to_bytes()),
&self.public_key_fingerprint, self.public_key_fingerprint,
&self.gossip_key_fingerprint, self.gossip_key_fingerprint,
self.verified_key.as_ref().map(|k| k.to_bytes()), self.verified_key.as_ref().map(|k| k.to_bytes()),
&self.verified_key_fingerprint, self.verified_key_fingerprint,
&self.addr, self.addr,
], ],
)?; ).await?;
} else if self.to_save == Some(ToSave::Timestamps) { } else if self.to_save == Some(ToSave::Timestamps) {
sql::execute( sql.execute(
self.context,
sql,
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \ "UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
WHERE addr=?;", WHERE addr=?;",
params![ paramsv![
self.last_seen, self.last_seen,
self.last_seen_autocrypt, self.last_seen_autocrypt,
self.gossip_timestamp, self.gossip_timestamp,
&self.addr self.addr
], ],
)?; )
.await?;
} }
Ok(()) Ok(())
@@ -479,9 +490,9 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use tempfile::TempDir; use tempfile::TempDir;
#[test] #[async_std::test]
fn test_peerstate_save_to_db() { async fn test_peerstate_save_to_db() {
let ctx = crate::test_utils::dummy_context(); let ctx = crate::test_utils::dummy_context().await;
let addr = "hello@mail.com"; let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public); let pub_key = crate::key::Key::from(alice_keypair().public);
@@ -504,11 +515,12 @@ mod tests {
}; };
assert!( assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(), peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"failed to save to db" "failed to save to db"
); );
let peerstate_new = Peerstate::from_addr(&ctx.ctx, &ctx.ctx.sql, addr) let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr)
.await
.expect("failed to load peerstate from db"); .expect("failed to load peerstate from db");
// clear to_save, as that is not persissted // clear to_save, as that is not persissted
@@ -516,13 +528,14 @@ mod tests {
assert_eq!(peerstate, peerstate_new); assert_eq!(peerstate, peerstate_new);
let peerstate_new2 = let peerstate_new2 =
Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint()) Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db"); .expect("failed to load peerstate from db");
assert_eq!(peerstate, peerstate_new2); assert_eq!(peerstate, peerstate_new2);
} }
#[test] #[async_std::test]
fn test_peerstate_double_create() { async fn test_peerstate_double_create() {
let ctx = crate::test_utils::dummy_context(); let ctx = crate::test_utils::dummy_context().await;
let addr = "hello@mail.com"; let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public); let pub_key = crate::key::Key::from(alice_keypair().public);
@@ -544,18 +557,18 @@ mod tests {
}; };
assert!( assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(), peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"failed to save" "failed to save"
); );
assert!( assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(), peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"double-call with create failed" "double-call with create failed"
); );
} }
#[test] #[async_std::test]
fn test_peerstate_with_empty_gossip_key_save_to_db() { async fn test_peerstate_with_empty_gossip_key_save_to_db() {
let ctx = crate::test_utils::dummy_context(); let ctx = crate::test_utils::dummy_context().await;
let addr = "hello@mail.com"; let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public); let pub_key = crate::key::Key::from(alice_keypair().public);
@@ -578,11 +591,12 @@ mod tests {
}; };
assert!( assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(), peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"failed to save" "failed to save"
); );
let peerstate_new = Peerstate::from_addr(&ctx.ctx, &ctx.ctx.sql, addr) let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr)
.await
.expect("failed to load peerstate from db"); .expect("failed to load peerstate from db");
// clear to_save, as that is not persissted // clear to_save, as that is not persissted

View File

@@ -238,124 +238,140 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<SignedPublicKeyOrSu
/// Encrypts `plain` text using `public_keys_for_encryption` /// Encrypts `plain` text using `public_keys_for_encryption`
/// and signs it using `private_key_for_signing`. /// and signs it using `private_key_for_signing`.
pub fn pk_encrypt( pub async fn pk_encrypt(
plain: &[u8], plain: &[u8],
public_keys_for_encryption: &Keyring, public_keys_for_encryption: Keyring,
private_key_for_signing: Option<&Key>, private_key_for_signing: Option<Key>,
) -> Result<String> { ) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain); let lit_msg = Message::new_literal_bytes("", plain);
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
.keys() async_std::task::spawn_blocking(move || {
.iter() let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
.filter_map(|key| { .keys()
key.as_ref() .iter()
.filter_map(|key| key.try_into().ok().and_then(select_pk_for_encryption))
.collect();
let pkeys_refs: Vec<&SignedPublicKeyOrSubkey> = pkeys.iter().collect();
let mut rng = thread_rng();
// TODO: measure time
let encrypted_msg = if let Some(ref private_key) = private_key_for_signing {
let skey: &SignedSecretKey = private_key
.try_into() .try_into()
.ok() .map_err(|_| format_err!("Invalid private key"))?;
.and_then(select_pk_for_encryption)
})
.collect();
let pkeys_refs: Vec<&SignedPublicKeyOrSubkey> = pkeys.iter().collect();
let mut rng = thread_rng(); lit_msg
.sign(skey, || "".into(), Default::default())
.and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB))
.and_then(|msg| msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs))
} else {
lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs)
};
// TODO: measure time let msg = encrypted_msg?;
let encrypted_msg = if let Some(private_key) = private_key_for_signing { let encoded_msg = msg.to_armored_string(None)?;
let skey: &SignedSecretKey = private_key
.try_into()
.map_err(|_| format_err!("Invalid private key"))?;
lit_msg Ok(encoded_msg)
.sign(skey, || "".into(), Default::default()) })
.and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB)) .await
.and_then(|msg| msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs))
} else {
lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs)
};
let msg = encrypted_msg?;
let encoded_msg = msg.to_armored_string(None)?;
Ok(encoded_msg)
} }
#[allow(clippy::implicit_hasher)] #[allow(clippy::implicit_hasher)]
pub fn pk_decrypt( pub async fn pk_decrypt(
ctext: &[u8], ctext: Vec<u8>,
private_keys_for_decryption: &Keyring, private_keys_for_decryption: Keyring,
public_keys_for_validation: &Keyring, public_keys_for_validation: Keyring,
ret_signature_fingerprints: Option<&mut HashSet<String>>, ret_signature_fingerprints: Option<&mut HashSet<String>>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let (msg, _) = Message::from_armor_single(Cursor::new(ctext))?; let msgs = async_std::task::spawn_blocking(move || {
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption let cursor = Cursor::new(ctext);
.keys() let (msg, _) = Message::from_armor_single(cursor)?;
.iter()
.filter_map(|key| { let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption
let k: &Key = &key; .keys()
k.try_into().ok() .iter()
}) .filter_map(|key| key.try_into().ok())
.collect(); .collect();
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
decryptor.collect::<pgp::errors::Result<Vec<_>>>()
})
.await?;
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
ensure!(!msgs.is_empty(), "No valid messages found"); ensure!(!msgs.is_empty(), "No valid messages found");
let dec_msg = &msgs[0]; let content = match msgs[0].get_content()? {
Some(content) => content,
None => bail!("Decrypted message is empty"),
};
if let Some(ret_signature_fingerprints) = ret_signature_fingerprints { if let Some(ret_signature_fingerprints) = ret_signature_fingerprints {
if !public_keys_for_validation.keys().is_empty() { if !public_keys_for_validation.is_empty() {
let pkeys: Vec<&SignedPublicKey> = public_keys_for_validation let fingerprints = async_std::task::spawn_blocking(move || {
.keys() let dec_msg = &msgs[0];
.iter()
.filter_map(|key| {
let k: &Key = &key;
k.try_into().ok()
})
.collect();
for pkey in &pkeys { let pkeys = public_keys_for_validation
if dec_msg.verify(&pkey.primary_key).is_ok() { .keys()
let fp = hex::encode_upper(pkey.fingerprint()); .iter()
ret_signature_fingerprints.insert(fp); .filter_map(|key| -> Option<&SignedPublicKey> { key.try_into().ok() });
let mut fingerprints = Vec::new();
for pkey in pkeys {
if dec_msg.verify(&pkey.primary_key).is_ok() {
let fp = hex::encode_upper(pkey.fingerprint());
fingerprints.push(fp);
}
} }
} fingerprints
})
.await;
ret_signature_fingerprints.extend(fingerprints);
} }
} }
match dec_msg.get_content()? { Ok(content)
Some(content) => Ok(content),
None => bail!("Decrypted message is empty"),
}
} }
/// Symmetric encryption. /// Symmetric encryption.
pub fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> { pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
let mut rng = thread_rng();
let lit_msg = Message::new_literal_bytes("", plain); let lit_msg = Message::new_literal_bytes("", plain);
let passphrase = passphrase.to_string();
let s2k = StringToKey::new_default(&mut rng); async_std::task::spawn_blocking(move || {
let msg = let mut rng = thread_rng();
lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase.into())?; let s2k = StringToKey::new_default(&mut rng);
let msg =
lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?;
let encoded_msg = msg.to_armored_string(None)?; let encoded_msg = msg.to_armored_string(None)?;
Ok(encoded_msg) Ok(encoded_msg)
})
.await
} }
/// Symmetric decryption. /// Symmetric decryption.
pub fn symm_decrypt<T: std::io::Read + std::io::Seek>( pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
passphrase: &str, passphrase: &str,
ctext: T, ctext: T,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let (enc_msg, _) = Message::from_armor_single(ctext)?; let (enc_msg, _) = Message::from_armor_single(ctext)?;
let decryptor = enc_msg.decrypt_with_password(|| passphrase.into())?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?; let passphrase = passphrase.to_string();
ensure!(!msgs.is_empty(), "No valid messages found"); async_std::task::spawn_blocking(move || {
let decryptor = enc_msg.decrypt_with_password(|| passphrase)?;
match msgs[0].get_content()? { let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
Some(content) => Ok(content), ensure!(!msgs.is_empty(), "No valid messages found");
None => bail!("Decrypted message is empty"),
} match msgs[0].get_content()? {
Some(content) => Ok(content),
None => bail!("Decrypted message is empty"),
}
})
.await
} }
#[cfg(test)] #[cfg(test)]
@@ -437,17 +453,17 @@ mod tests {
/// A cyphertext encrypted to Alice & Bob, signed by Alice. /// A cyphertext encrypted to Alice & Bob, signed by Alice.
static ref CTEXT_SIGNED: String = { static ref CTEXT_SIGNED: String = {
let mut keyring = Keyring::default(); let mut keyring = Keyring::default();
keyring.add_owned(KEYS.alice_public.clone()); keyring.add(KEYS.alice_public.clone());
keyring.add_ref(&KEYS.bob_public); keyring.add(KEYS.bob_public.clone());
pk_encrypt(CLEARTEXT, &keyring, Some(&KEYS.alice_secret)).unwrap() smol::block_on(pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))).unwrap()
}; };
/// A cyphertext encrypted to Alice & Bob, not signed. /// A cyphertext encrypted to Alice & Bob, not signed.
static ref CTEXT_UNSIGNED: String = { static ref CTEXT_UNSIGNED: String = {
let mut keyring = Keyring::default(); let mut keyring = Keyring::default();
keyring.add_owned(KEYS.alice_public.clone()); keyring.add(KEYS.alice_public.clone());
keyring.add_ref(&KEYS.bob_public); keyring.add(KEYS.bob_public.clone());
pk_encrypt(CLEARTEXT, &keyring, None).unwrap() smol::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
}; };
} }
@@ -463,20 +479,21 @@ mod tests {
assert!(CTEXT_UNSIGNED.starts_with("-----BEGIN PGP MESSAGE-----")); assert!(CTEXT_UNSIGNED.starts_with("-----BEGIN PGP MESSAGE-----"));
} }
#[test] #[async_std::test]
fn test_decrypt_singed() { async fn test_decrypt_singed() {
// Check decrypting as Alice // Check decrypting as Alice
let mut decrypt_keyring = Keyring::default(); let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.alice_secret); decrypt_keyring.add(KEYS.alice_secret.clone());
let mut sig_check_keyring = Keyring::default(); let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public); sig_check_keyring.add(KEYS.alice_public.clone());
let mut valid_signatures: HashSet<String> = Default::default(); let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt( let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(), CTEXT_SIGNED.as_bytes().to_vec(),
&decrypt_keyring, decrypt_keyring,
&sig_check_keyring, sig_check_keyring,
Some(&mut valid_signatures), Some(&mut valid_signatures),
) )
.await
.map_err(|err| println!("{:?}", err)) .map_err(|err| println!("{:?}", err))
.unwrap(); .unwrap();
assert_eq!(plain, CLEARTEXT); assert_eq!(plain, CLEARTEXT);
@@ -484,89 +501,94 @@ mod tests {
// Check decrypting as Bob // Check decrypting as Bob
let mut decrypt_keyring = Keyring::default(); let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret); decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::default(); let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public); sig_check_keyring.add(KEYS.alice_public.clone());
let mut valid_signatures: HashSet<String> = Default::default(); let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt( let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(), CTEXT_SIGNED.as_bytes().to_vec(),
&decrypt_keyring, decrypt_keyring,
&sig_check_keyring, sig_check_keyring,
Some(&mut valid_signatures), Some(&mut valid_signatures),
) )
.await
.map_err(|err| println!("{:?}", err)) .map_err(|err| println!("{:?}", err))
.unwrap(); .unwrap();
assert_eq!(plain, CLEARTEXT); assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1); assert_eq!(valid_signatures.len(), 1);
} }
#[test] #[async_std::test]
fn test_decrypt_no_sig_check() { async fn test_decrypt_no_sig_check() {
let mut keyring = Keyring::default(); let mut keyring = Keyring::default();
keyring.add_ref(&KEYS.alice_secret); keyring.add(KEYS.alice_secret.clone());
let empty_keyring = Keyring::default(); let empty_keyring = Keyring::default();
let mut valid_signatures: HashSet<String> = Default::default(); let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt( let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(), CTEXT_SIGNED.as_bytes().to_vec(),
&keyring, keyring,
&empty_keyring, empty_keyring,
Some(&mut valid_signatures), Some(&mut valid_signatures),
) )
.await
.unwrap(); .unwrap();
assert_eq!(plain, CLEARTEXT); assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0); assert_eq!(valid_signatures.len(), 0);
} }
#[test] #[async_std::test]
fn test_decrypt_signed_no_key() { async fn test_decrypt_signed_no_key() {
// The validation does not have the public key of the signer. // The validation does not have the public key of the signer.
let mut decrypt_keyring = Keyring::default(); let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret); decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::default(); let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.bob_public); sig_check_keyring.add(KEYS.bob_public.clone());
let mut valid_signatures: HashSet<String> = Default::default(); let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt( let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(), CTEXT_SIGNED.as_bytes().to_vec(),
&decrypt_keyring, decrypt_keyring,
&sig_check_keyring, sig_check_keyring,
Some(&mut valid_signatures), Some(&mut valid_signatures),
) )
.await
.unwrap(); .unwrap();
assert_eq!(plain, CLEARTEXT); assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0); assert_eq!(valid_signatures.len(), 0);
} }
#[test] #[async_std::test]
fn test_decrypt_unsigned() { async fn test_decrypt_unsigned() {
let mut decrypt_keyring = Keyring::default(); let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret); decrypt_keyring.add(KEYS.bob_secret.clone());
let sig_check_keyring = Keyring::default(); let sig_check_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.alice_public); decrypt_keyring.add(KEYS.alice_public.clone());
let mut valid_signatures: HashSet<String> = Default::default(); let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt( let plain = pk_decrypt(
CTEXT_UNSIGNED.as_bytes(), CTEXT_UNSIGNED.as_bytes().to_vec(),
&decrypt_keyring, decrypt_keyring,
&sig_check_keyring, sig_check_keyring,
Some(&mut valid_signatures), Some(&mut valid_signatures),
) )
.await
.unwrap(); .unwrap();
assert_eq!(plain, CLEARTEXT); assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0); assert_eq!(valid_signatures.len(), 0);
} }
#[test] #[async_std::test]
fn test_decrypt_signed_no_sigret() { async fn test_decrypt_signed_no_sigret() {
// Check decrypting signed cyphertext without providing the HashSet for signatures. // Check decrypting signed cyphertext without providing the HashSet for signatures.
let mut decrypt_keyring = Keyring::default(); let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret); decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::default(); let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public); sig_check_keyring.add(KEYS.alice_public.clone());
let plain = pk_decrypt( let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(), CTEXT_SIGNED.as_bytes().to_vec(),
&decrypt_keyring, decrypt_keyring,
&sig_check_keyring, sig_check_keyring,
None, None,
) )
.await
.unwrap(); .unwrap();
assert_eq!(plain, CLEARTEXT); assert_eq!(plain, CLEARTEXT);
} }

213
src/qr.rs
View File

@@ -2,7 +2,6 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
use reqwest::Url;
use serde::Deserialize; use serde::Deserialize;
use crate::chat; use crate::chat;
@@ -44,23 +43,23 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
/// Check a scanned QR code. /// Check a scanned QR code.
/// The function should be called after a QR code is scanned. /// The function should be called after a QR code is scanned.
/// The function takes the raw text scanned and checks what can be done with it. /// The function takes the raw text scanned and checks what can be done with it.
pub fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot { pub async fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
let qr = qr.as_ref(); let qr = qr.as_ref();
info!(context, "Scanned QR code: {}", qr); info!(context, "Scanned QR code: {}", qr);
if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) { if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
decode_openpgp(context, qr) decode_openpgp(context, qr).await
} else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) { } else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) {
decode_account(context, qr) decode_account(context, qr)
} else if qr.starts_with(MAILTO_SCHEME) { } else if qr.starts_with(MAILTO_SCHEME) {
decode_mailto(context, qr) decode_mailto(context, qr).await
} else if qr.starts_with(SMTP_SCHEME) { } else if qr.starts_with(SMTP_SCHEME) {
decode_smtp(context, qr) decode_smtp(context, qr).await
} else if qr.starts_with(MATMSG_SCHEME) { } else if qr.starts_with(MATMSG_SCHEME) {
decode_matmsg(context, qr) decode_matmsg(context, qr).await
} else if qr.starts_with(VCARD_SCHEME) { } else if qr.starts_with(VCARD_SCHEME) {
decode_vcard(context, qr) decode_vcard(context, qr).await
} else if qr.starts_with(HTTP_SCHEME) || qr.starts_with(HTTPS_SCHEME) { } else if qr.starts_with(HTTP_SCHEME) || qr.starts_with(HTTPS_SCHEME) {
Lot::from_url(qr) Lot::from_url(qr)
} else { } else {
@@ -70,7 +69,7 @@ pub fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH` /// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH` /// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
fn decode_openpgp(context: &Context, qr: &str) -> Lot { async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
let payload = &qr[OPENPGP4FPR_SCHEME.len()..]; let payload = &qr[OPENPGP4FPR_SCHEME.len()..];
let (fingerprint, fragment) = match payload.find('#').map(|offset| { let (fingerprint, fragment) = match payload.find('#').map(|offset| {
@@ -136,15 +135,10 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
return format_err!("Bad fingerprint length in QR code").into(); return format_err!("Bad fingerprint length in QR code").into();
} }
println!(
"{:?} {:?} {:?} {:?} {:?} {:?} {:?}",
addr, name, invitenumber, auth, grpid, grpname, fingerprint
);
let mut lot = Lot::new(); let mut lot = Lot::new();
// retrieve known state for this fingerprint // retrieve known state for this fingerprint
let peerstate = Peerstate::from_fingerprint(context, &context.sql, &fingerprint); let peerstate = Peerstate::from_fingerprint(context, &context.sql, &fingerprint).await;
if invitenumber.is_none() || auth.is_none() { if invitenumber.is_none() || auth.is_none() {
if let Some(peerstate) = peerstate { if let Some(peerstate) = peerstate {
@@ -156,13 +150,15 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
peerstate.addr.clone(), peerstate.addr.clone(),
Origin::UnhandledQrScan, Origin::UnhandledQrScan,
) )
.await
.map(|(id, _)| id) .map(|(id, _)| id)
.unwrap_or_default(); .unwrap_or_default();
let (id, _) = chat::create_or_lookup_by_contact_id(context, lot.id, Blocked::Deaddrop) let (id, _) = chat::create_or_lookup_by_contact_id(context, lot.id, Blocked::Deaddrop)
.await
.unwrap_or_default(); .unwrap_or_default();
chat::add_info_msg(context, id, format!("{} verified.", peerstate.addr)); chat::add_info_msg(context, id, format!("{} verified.", peerstate.addr)).await;
} else { } else {
lot.state = LotState::QrFprWithoutAddr; lot.state = LotState::QrFprWithoutAddr;
lot.text1 = Some(dc_format_fingerprint(&fingerprint)); lot.text1 = Some(dc_format_fingerprint(&fingerprint));
@@ -176,6 +172,7 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
lot.state = LotState::QrAskVerifyContact; lot.state = LotState::QrAskVerifyContact;
} }
lot.id = Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan) lot.id = Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan)
.await
.map(|(id, _)| id) .map(|(id, _)| id)
.unwrap_or_default(); .unwrap_or_default();
@@ -195,7 +192,7 @@ fn decode_account(_context: &Context, qr: &str) -> Lot {
let mut lot = Lot::new(); let mut lot = Lot::new();
if let Ok(url) = Url::parse(payload) { if let Ok(url) = url::Url::parse(payload) {
if url.scheme() == "https" { if url.scheme() == "https" {
lot.state = LotState::QrAccount; lot.state = LotState::QrAccount;
lot.text1 = url.host_str().map(|x| x.to_string()); lot.text1 = url.host_str().map(|x| x.to_string());
@@ -220,31 +217,22 @@ struct CreateAccountResponse {
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters, /// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
/// download additional information from the contained url and set the 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 /// 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> { pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
let url_str = &qr[DCACCOUNT_SCHEME.len()..]; let url_str = &qr[DCACCOUNT_SCHEME.len()..];
let response = reqwest::blocking::Client::new().post(url_str).send(); let response: Result<CreateAccountResponse, surf::Error> =
surf::post(url_str).recv_json().await;
if response.is_err() { if response.is_err() {
bail!("Cannot create account, request to {} failed", url_str); bail!("Cannot create account, request to {} failed", url_str);
} }
let response = response.unwrap(); let parsed = response.unwrap();
if !response.status().is_success() {
bail!("Request to {} unsuccessful: {:?}", url_str, response);
}
let parsed: reqwest::Result<CreateAccountResponse> = response.json(); context
if parsed.is_err() { .set_config(Config::Addr, Some(&parsed.email))
bail!( .await?;
"Failed to parse JSON response from {}: error: {:?}", context
url_str, .set_config(Config::MailPw, Some(&parsed.password))
parsed .await?;
);
}
println!("response: {:?}", &parsed);
let parsed = parsed.unwrap();
context.set_config(Config::Addr, Some(&parsed.email))?;
context.set_config(Config::MailPw, Some(&parsed.password))?;
Ok(()) Ok(())
} }
@@ -252,7 +240,7 @@ pub fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
/// Extract address for the mailto scheme. /// Extract address for the mailto scheme.
/// ///
/// Scheme: `mailto:addr...?subject=...&body=..` /// Scheme: `mailto:addr...?subject=...&body=..`
fn decode_mailto(context: &Context, qr: &str) -> Lot { async fn decode_mailto(context: &Context, qr: &str) -> Lot {
let payload = &qr[MAILTO_SCHEME.len()..]; let payload = &qr[MAILTO_SCHEME.len()..];
let addr = if let Some(query_index) = payload.find('?') { let addr = if let Some(query_index) = payload.find('?') {
@@ -267,13 +255,13 @@ fn decode_mailto(context: &Context, qr: &str) -> Lot {
}; };
let name = "".to_string(); let name = "".to_string();
Lot::from_address(context, name, addr) Lot::from_address(context, name, addr).await
} }
/// Extract address for the smtp scheme. /// Extract address for the smtp scheme.
/// ///
/// Scheme: `SMTP:addr...:subject...:body...` /// Scheme: `SMTP:addr...:subject...:body...`
fn decode_smtp(context: &Context, qr: &str) -> Lot { async fn decode_smtp(context: &Context, qr: &str) -> Lot {
let payload = &qr[SMTP_SCHEME.len()..]; let payload = &qr[SMTP_SCHEME.len()..];
let addr = if let Some(query_index) = payload.find(':') { let addr = if let Some(query_index) = payload.find(':') {
@@ -287,7 +275,7 @@ fn decode_smtp(context: &Context, qr: &str) -> Lot {
Err(err) => return err.into(), Err(err) => return err.into(),
}; };
let name = "".to_string(); let name = "".to_string();
Lot::from_address(context, name, addr) Lot::from_address(context, name, addr).await
} }
/// Extract address for the matmsg scheme. /// Extract address for the matmsg scheme.
@@ -295,7 +283,7 @@ fn decode_smtp(context: &Context, qr: &str) -> Lot {
/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;` /// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
/// ///
/// There may or may not be linebreaks after the fields. /// There may or may not be linebreaks after the fields.
fn decode_matmsg(context: &Context, qr: &str) -> Lot { async fn decode_matmsg(context: &Context, qr: &str) -> Lot {
// Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field. // Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
// we ignore this case. // we ignore this case.
let addr = if let Some(to_index) = qr.find("TO:") { let addr = if let Some(to_index) = qr.find("TO:") {
@@ -315,7 +303,7 @@ fn decode_matmsg(context: &Context, qr: &str) -> Lot {
}; };
let name = "".to_string(); let name = "".to_string();
Lot::from_address(context, name, addr) Lot::from_address(context, name, addr).await
} }
lazy_static! { lazy_static! {
@@ -328,7 +316,7 @@ lazy_static! {
/// Extract address for the matmsg scheme. /// Extract address for the matmsg scheme.
/// ///
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...; /// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;
fn decode_vcard(context: &Context, qr: &str) -> Lot { async fn decode_vcard(context: &Context, qr: &str) -> Lot {
let name = VCARD_NAME_RE let name = VCARD_NAME_RE
.captures(qr) .captures(qr)
.map(|caps| { .map(|caps| {
@@ -348,7 +336,7 @@ fn decode_vcard(context: &Context, qr: &str) -> Lot {
return format_err!("Bad e-mail address").into(); return format_err!("Bad e-mail address").into();
}; };
Lot::from_address(context, name, addr) Lot::from_address(context, name, addr).await
} }
impl Lot { impl Lot {
@@ -368,10 +356,10 @@ impl Lot {
l l
} }
pub fn from_address(context: &Context, name: String, addr: String) -> Self { pub async fn from_address(context: &Context, name: String, addr: String) -> Self {
let mut l = Lot::new(); let mut l = Lot::new();
l.state = LotState::QrAddr; l.state = LotState::QrAddr;
l.id = match Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan) { l.id = match Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan).await {
Ok((id, _)) => id, Ok((id, _)) => id,
Err(err) => return err.into(), Err(err) => return err.into(),
}; };
@@ -397,11 +385,11 @@ mod tests {
use crate::test_utils::dummy_context; use crate::test_utils::dummy_context;
#[test] #[async_std::test]
fn test_decode_http() { async fn test_decode_http() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr(&ctx.ctx, "http://www.hello.com"); let res = check_qr(&ctx.ctx, "http://www.hello.com").await;
assert_eq!(res.get_state(), LotState::QrUrl); assert_eq!(res.get_state(), LotState::QrUrl);
assert_eq!(res.get_id(), 0); assert_eq!(res.get_id(), 0);
@@ -409,11 +397,11 @@ mod tests {
assert!(res.get_text2().is_none()); assert!(res.get_text2().is_none());
} }
#[test] #[async_std::test]
fn test_decode_https() { async fn test_decode_https() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr(&ctx.ctx, "https://www.hello.com"); let res = check_qr(&ctx.ctx, "https://www.hello.com").await;
assert_eq!(res.get_state(), LotState::QrUrl); assert_eq!(res.get_state(), LotState::QrUrl);
assert_eq!(res.get_id(), 0); assert_eq!(res.get_id(), 0);
@@ -421,11 +409,11 @@ mod tests {
assert!(res.get_text2().is_none()); assert!(res.get_text2().is_none());
} }
#[test] #[async_std::test]
fn test_decode_text() { async fn test_decode_text() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr(&ctx.ctx, "I am so cool"); let res = check_qr(&ctx.ctx, "I am so cool").await;
assert_eq!(res.get_state(), LotState::QrText); assert_eq!(res.get_state(), LotState::QrText);
assert_eq!(res.get_id(), 0); assert_eq!(res.get_id(), 0);
@@ -433,88 +421,90 @@ mod tests {
assert!(res.get_text2().is_none()); assert!(res.get_text2().is_none());
} }
#[test] #[async_std::test]
fn test_decode_vcard() { async fn test_decode_vcard() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD" "BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD"
); ).await;
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.get_state(), LotState::QrAddr); assert_eq!(res.get_state(), LotState::QrAddr);
assert_ne!(res.get_id(), 0); assert_ne!(res.get_id(), 0);
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap();
assert_eq!(contact.get_addr(), "stress@test.local"); assert_eq!(contact.get_addr(), "stress@test.local");
assert_eq!(contact.get_name(), "First Last"); assert_eq!(contact.get_name(), "First Last");
} }
#[test] #[async_std::test]
fn test_decode_matmsg() { async fn test_decode_matmsg() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"MATMSG:TO:\n\nstress@test.local ; \n\nSUB:\n\nSubject here\n\nBODY:\n\nhelloworld\n;;", "MATMSG:TO:\n\nstress@test.local ; \n\nSUB:\n\nSubject here\n\nBODY:\n\nhelloworld\n;;",
); )
.await;
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.get_state(), LotState::QrAddr); assert_eq!(res.get_state(), LotState::QrAddr);
assert_ne!(res.get_id(), 0); assert_ne!(res.get_id(), 0);
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap();
assert_eq!(contact.get_addr(), "stress@test.local"); assert_eq!(contact.get_addr(), "stress@test.local");
} }
#[test] #[async_std::test]
fn test_decode_mailto() { async fn test_decode_mailto() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"mailto:stress@test.local?subject=hello&body=world", "mailto:stress@test.local?subject=hello&body=world",
); )
.await;
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.get_state(), LotState::QrAddr); assert_eq!(res.get_state(), LotState::QrAddr);
assert_ne!(res.get_id(), 0); assert_ne!(res.get_id(), 0);
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap();
assert_eq!(contact.get_addr(), "stress@test.local"); assert_eq!(contact.get_addr(), "stress@test.local");
let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org"); let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org").await;
assert_eq!(res.get_state(), LotState::QrAddr); assert_eq!(res.get_state(), LotState::QrAddr);
assert_ne!(res.get_id(), 0); assert_ne!(res.get_id(), 0);
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap();
assert_eq!(contact.get_addr(), "no-questionmark@example.org"); assert_eq!(contact.get_addr(), "no-questionmark@example.org");
let res = check_qr(&ctx.ctx, "mailto:no-addr"); let res = check_qr(&ctx.ctx, "mailto:no-addr").await;
assert_eq!(res.get_state(), LotState::QrError); assert_eq!(res.get_state(), LotState::QrError);
assert!(res.get_text1().is_some()); assert!(res.get_text1().is_some());
} }
#[test] #[async_std::test]
fn test_decode_smtp() { async fn test_decode_smtp() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld"); let res = check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await;
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.get_state(), LotState::QrAddr); assert_eq!(res.get_state(), LotState::QrAddr);
assert_ne!(res.get_id(), 0); assert_ne!(res.get_id(), 0);
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap();
assert_eq!(contact.get_addr(), "stress@test.local"); assert_eq!(contact.get_addr(), "stress@test.local");
} }
#[test] #[async_std::test]
fn test_decode_openpgp_group() { async fn test_decode_openpgp_group() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" "OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
); ).await;
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.get_state(), LotState::QrAskVerifyGroup); assert_eq!(res.get_state(), LotState::QrAskVerifyGroup);
@@ -525,25 +515,25 @@ mod tests {
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL" "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
); ).await;
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.get_state(), LotState::QrAskVerifyGroup); assert_eq!(res.get_state(), LotState::QrAskVerifyGroup);
assert_ne!(res.get_id(), 0); assert_ne!(res.get_id(), 0);
assert_eq!(res.get_text1().unwrap(), "test ? test !"); assert_eq!(res.get_text1().unwrap(), "test ? test !");
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap();
assert_eq!(contact.get_addr(), "cli@deltachat.de"); assert_eq!(contact.get_addr(), "cli@deltachat.de");
} }
#[test] #[async_std::test]
fn test_decode_openpgp_secure_join() { async fn test_decode_openpgp_secure_join() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB" "OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
); ).await;
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.get_state(), LotState::QrAskVerifyContact); assert_eq!(res.get_state(), LotState::QrAskVerifyContact);
@@ -553,25 +543,26 @@ mod tests {
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB" "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
); ).await;
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res.get_state(), LotState::QrAskVerifyContact); assert_eq!(res.get_state(), LotState::QrAskVerifyContact);
assert_ne!(res.get_id(), 0); assert_ne!(res.get_id(), 0);
let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).unwrap(); let contact = Contact::get_by_id(&ctx.ctx, res.get_id()).await.unwrap();
assert_eq!(contact.get_addr(), "cli@deltachat.de"); assert_eq!(contact.get_addr(), "cli@deltachat.de");
assert_eq!(contact.get_name(), "Jörn P. P."); assert_eq!(contact.get_name(), "Jörn P. P.");
} }
#[test] #[async_std::test]
fn test_decode_openpgp_without_addr() { async fn test_decode_openpgp_without_addr() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"OPENPGP4FPR:1234567890123456789012345678901234567890", "OPENPGP4FPR:1234567890123456789012345678901234567890",
); )
.await;
assert_eq!(res.get_state(), LotState::QrFprWithoutAddr); assert_eq!(res.get_state(), LotState::QrFprWithoutAddr);
assert_eq!( assert_eq!(
res.get_text1().unwrap(), res.get_text1().unwrap(),
@@ -584,7 +575,8 @@ mod tests {
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"openpgp4fpr:1234567890123456789012345678901234567890", "openpgp4fpr:1234567890123456789012345678901234567890",
); )
.await;
assert_eq!(res.get_state(), LotState::QrFprWithoutAddr); assert_eq!(res.get_state(), LotState::QrFprWithoutAddr);
assert_eq!( assert_eq!(
res.get_text1().unwrap(), res.get_text1().unwrap(),
@@ -592,19 +584,20 @@ mod tests {
); );
assert_eq!(res.get_id(), 0); assert_eq!(res.get_id(), 0);
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890"); let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await;
assert_eq!(res.get_state(), LotState::QrError); assert_eq!(res.get_state(), LotState::QrError);
assert_eq!(res.get_id(), 0); assert_eq!(res.get_id(), 0);
} }
#[test] #[async_std::test]
fn test_decode_account() { async fn test_decode_account() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", "DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
); )
.await;
assert_eq!(res.get_state(), LotState::QrAccount); assert_eq!(res.get_state(), LotState::QrAccount);
assert_eq!(res.get_text1().unwrap(), "example.org"); assert_eq!(res.get_text1().unwrap(), "example.org");
@@ -612,19 +605,20 @@ mod tests {
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", "dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
); )
.await;
assert_eq!(res.get_state(), LotState::QrAccount); assert_eq!(res.get_state(), LotState::QrAccount);
assert_eq!(res.get_text1().unwrap(), "example.org"); assert_eq!(res.get_text1().unwrap(), "example.org");
} }
#[test] #[async_std::test]
fn test_decode_account_bad_scheme() { async fn test_decode_account_bad_scheme() {
let ctx = dummy_context(); let ctx = dummy_context().await;
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", "DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
); )
.await;
assert_eq!(res.get_state(), LotState::QrError); assert_eq!(res.get_state(), LotState::QrError);
assert!(res.get_text1().is_some()); assert!(res.get_text1().is_some());
@@ -632,7 +626,8 @@ mod tests {
let res = check_qr( let res = check_qr(
&ctx.ctx, &ctx.ctx,
"dcaccount:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", "dcaccount:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
); )
.await;
assert_eq!(res.get_state(), LotState::QrError); assert_eq!(res.get_state(), LotState::QrError);
assert!(res.get_text1().is_some()); assert!(res.get_text1().is_some());
} }

592
src/scheduler.rs Normal file
View File

@@ -0,0 +1,592 @@
use async_std::prelude::*;
use async_std::sync::{channel, Receiver, Sender};
use async_std::task;
use crate::context::Context;
use crate::imap::Imap;
use crate::job::{self, Thread};
use crate::smtp::Smtp;
pub(crate) struct StopToken;
/// Job and connection scheduler.
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Scheduler {
Stopped,
Running {
inbox: ImapConnectionState,
inbox_handle: Option<task::JoinHandle<()>>,
mvbox: ImapConnectionState,
mvbox_handle: Option<task::JoinHandle<()>>,
sentbox: ImapConnectionState,
sentbox_handle: Option<task::JoinHandle<()>>,
smtp: SmtpConnectionState,
smtp_handle: Option<task::JoinHandle<()>>,
},
}
impl Context {
/// Indicate that the network likely has come back.
pub async fn maybe_network(&self) {
self.scheduler.read().await.maybe_network().await;
}
pub(crate) async fn interrupt_inbox(&self, probe_network: bool) {
self.scheduler
.read()
.await
.interrupt_inbox(probe_network)
.await;
}
pub(crate) async fn interrupt_sentbox(&self, probe_network: bool) {
self.scheduler
.read()
.await
.interrupt_sentbox(probe_network)
.await;
}
pub(crate) async fn interrupt_mvbox(&self, probe_network: bool) {
self.scheduler
.read()
.await
.interrupt_mvbox(probe_network)
.await;
}
pub(crate) async fn interrupt_smtp(&self, probe_network: bool) {
self.scheduler
.read()
.await
.interrupt_smtp(probe_network)
.await;
}
}
async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConnectionHandlers) {
use futures::future::FutureExt;
info!(ctx, "starting inbox loop");
let ImapConnectionHandlers {
mut connection,
stop_receiver,
shutdown_sender,
} = inbox_handlers;
let ctx1 = ctx.clone();
let fut = async move {
started.send(()).await;
let ctx = ctx1;
if let Err(err) = connection.connect_configured(&ctx).await {
error!(ctx, "{}", err);
return;
}
// track number of continously executed jobs
let mut jobs_loaded = 0;
let mut probe_network = false;
loop {
match job::load_next(&ctx, Thread::Imap, probe_network).await {
Some(job) if jobs_loaded <= 20 => {
jobs_loaded += 1;
job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await;
probe_network = false;
}
Some(job) => {
// Let the fetch run, but return back to the job afterwards.
info!(ctx, "postponing imap-job {} to run fetch...", job);
jobs_loaded = 0;
fetch(&ctx, &mut connection).await;
}
None => {
jobs_loaded = 0;
probe_network = fetch_idle(&ctx, &mut connection).await;
}
}
}
};
stop_receiver
.recv()
.map(|_| {
info!(ctx, "shutting down inbox loop");
})
.race(fut)
.await;
shutdown_sender.send(()).await;
}
async fn fetch(ctx: &Context, connection: &mut Imap) {
match get_watch_folder(&ctx, "configured_inbox_folder").await {
Some(watch_folder) => {
// fetch
connection
.fetch(&ctx, &watch_folder)
.await
.unwrap_or_else(|err| {
error!(ctx, "{}", err);
});
}
None => {
warn!(ctx, "Can not fetch inbox folder, not set");
connection.fake_idle(&ctx, None).await;
}
}
}
async fn fetch_idle(ctx: &Context, connection: &mut Imap) -> bool {
match get_watch_folder(&ctx, "configured_inbox_folder").await {
Some(watch_folder) => {
// fetch
connection
.fetch(&ctx, &watch_folder)
.await
.unwrap_or_else(|err| {
error!(ctx, "{}", err);
});
// idle
if connection.can_idle() {
connection
.idle(&ctx, Some(watch_folder))
.await
.unwrap_or_else(|err| {
error!(ctx, "{}", err);
false
})
} else {
connection.fake_idle(&ctx, Some(watch_folder)).await
}
}
None => {
warn!(ctx, "Can not watch inbox folder, not set");
connection.fake_idle(&ctx, None).await
}
}
}
async fn simple_imap_loop(
ctx: Context,
started: Sender<()>,
inbox_handlers: ImapConnectionHandlers,
folder: impl AsRef<str>,
) {
use futures::future::FutureExt;
info!(ctx, "starting simple loop for {}", folder.as_ref());
let ImapConnectionHandlers {
mut connection,
stop_receiver,
shutdown_sender,
} = inbox_handlers;
let ctx1 = ctx.clone();
let fut = async move {
started.send(()).await;
let ctx = ctx1;
if let Err(err) = connection.connect_configured(&ctx).await {
error!(ctx, "{}", err);
return;
}
loop {
match get_watch_folder(&ctx, folder.as_ref()).await {
Some(watch_folder) => {
// fetch
connection
.fetch(&ctx, &watch_folder)
.await
.unwrap_or_else(|err| {
error!(ctx, "{}", err);
});
// idle
if connection.can_idle() {
connection
.idle(&ctx, Some(watch_folder))
.await
.unwrap_or_else(|err| {
error!(ctx, "{}", err);
false
});
} else {
connection.fake_idle(&ctx, Some(watch_folder)).await;
}
}
None => {
warn!(
&ctx,
"No watch folder found for {}, skipping",
folder.as_ref()
);
connection.fake_idle(&ctx, None).await;
}
}
}
};
stop_receiver
.recv()
.map(|_| {
info!(ctx, "shutting down simple loop");
})
.race(fut)
.await;
shutdown_sender.send(()).await;
}
async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnectionHandlers) {
use futures::future::FutureExt;
info!(ctx, "starting smtp loop");
let SmtpConnectionHandlers {
mut connection,
stop_receiver,
shutdown_sender,
idle_interrupt_receiver,
} = smtp_handlers;
let ctx1 = ctx.clone();
let fut = async move {
started.send(()).await;
let ctx = ctx1;
let mut probe_network = false;
loop {
match job::load_next(&ctx, Thread::Smtp, probe_network).await {
Some(job) => {
info!(ctx, "executing smtp job");
job::perform_job(&ctx, job::Connection::Smtp(&mut connection), job).await;
probe_network = false;
}
None => {
// Fake Idle
info!(ctx, "smtp fake idle - started");
probe_network = idle_interrupt_receiver.recv().await.unwrap_or_default();
info!(ctx, "smtp fake idle - interrupted")
}
}
}
};
stop_receiver
.recv()
.map(|_| {
info!(ctx, "shutting down smtp loop");
})
.race(fut)
.await;
shutdown_sender.send(()).await;
}
impl Scheduler {
/// Start the scheduler, panics if it is already running.
pub async fn start(&mut self, ctx: Context) {
let (mvbox, mvbox_handlers) = ImapConnectionState::new();
let (sentbox, sentbox_handlers) = ImapConnectionState::new();
let (smtp, smtp_handlers) = SmtpConnectionState::new();
let (inbox, inbox_handlers) = ImapConnectionState::new();
*self = Scheduler::Running {
inbox,
mvbox,
sentbox,
smtp,
inbox_handle: None,
mvbox_handle: None,
sentbox_handle: None,
smtp_handle: None,
};
let (inbox_start_send, inbox_start_recv) = channel(1);
if let Scheduler::Running { inbox_handle, .. } = self {
let ctx1 = ctx.clone();
*inbox_handle = Some(task::spawn(async move {
inbox_loop(ctx1, inbox_start_send, inbox_handlers).await
}));
}
let (mvbox_start_send, mvbox_start_recv) = channel(1);
if let Scheduler::Running { mvbox_handle, .. } = self {
let ctx1 = ctx.clone();
*mvbox_handle = Some(task::spawn(async move {
simple_imap_loop(
ctx1,
mvbox_start_send,
mvbox_handlers,
"configured_mvbox_folder",
)
.await
}));
}
let (sentbox_start_send, sentbox_start_recv) = channel(1);
if let Scheduler::Running { sentbox_handle, .. } = self {
let ctx1 = ctx.clone();
*sentbox_handle = Some(task::spawn(async move {
simple_imap_loop(
ctx1,
sentbox_start_send,
sentbox_handlers,
"configured_sentbox_folder",
)
.await
}));
}
let (smtp_start_send, smtp_start_recv) = channel(1);
if let Scheduler::Running { smtp_handle, .. } = self {
let ctx1 = ctx.clone();
*smtp_handle = Some(task::spawn(async move {
smtp_loop(ctx1, smtp_start_send, smtp_handlers).await
}));
}
// wait for all loops to be started
if let Err(err) = inbox_start_recv
.recv()
.try_join(mvbox_start_recv.recv())
.try_join(sentbox_start_recv.recv())
.try_join(smtp_start_recv.recv())
.await
{
error!(ctx, "failed to start scheduler: {}", err);
}
info!(ctx, "scheduler is running");
}
async fn maybe_network(&self) {
if !self.is_running() {
return;
}
self.interrupt_inbox(true)
.join(self.interrupt_mvbox(true))
.join(self.interrupt_sentbox(true))
.join(self.interrupt_smtp(true))
.await;
}
async fn interrupt_inbox(&self, probe_network: bool) {
if let Scheduler::Running { ref inbox, .. } = self {
inbox.interrupt(probe_network).await;
}
}
async fn interrupt_mvbox(&self, probe_network: bool) {
if let Scheduler::Running { ref mvbox, .. } = self {
mvbox.interrupt(probe_network).await;
}
}
async fn interrupt_sentbox(&self, probe_network: bool) {
if let Scheduler::Running { ref sentbox, .. } = self {
sentbox.interrupt(probe_network).await;
}
}
async fn interrupt_smtp(&self, probe_network: bool) {
if let Scheduler::Running { ref smtp, .. } = self {
smtp.interrupt(probe_network).await;
}
}
/// Halts the scheduler, must be called first, and then `stop`.
pub(crate) async fn pre_stop(&self) -> StopToken {
match self {
Scheduler::Stopped => {
panic!("WARN: already stopped");
}
Scheduler::Running {
inbox,
mvbox,
sentbox,
smtp,
..
} => {
inbox
.stop()
.join(mvbox.stop())
.join(sentbox.stop())
.join(smtp.stop())
.await;
StopToken
}
}
}
/// Halt the scheduler, must only be called after pre_stop.
pub(crate) async fn stop(&mut self, _t: StopToken) {
match self {
Scheduler::Stopped => {
panic!("WARN: already stopped");
}
Scheduler::Running {
inbox_handle,
mvbox_handle,
sentbox_handle,
smtp_handle,
..
} => {
inbox_handle.take().expect("inbox not started").await;
mvbox_handle.take().expect("mvbox not started").await;
sentbox_handle.take().expect("sentbox not started").await;
smtp_handle.take().expect("smtp not started").await;
*self = Scheduler::Stopped;
}
}
}
/// Check if the scheduler is running.
pub fn is_running(&self) -> bool {
match self {
Scheduler::Running { .. } => true,
_ => false,
}
}
}
/// Connection state logic shared between imap and smtp connections.
#[derive(Debug)]
struct ConnectionState {
/// Channel to notify that shutdown has completed.
shutdown_receiver: Receiver<()>,
/// Channel to interrupt the whole connection.
stop_sender: Sender<()>,
/// Channel to interrupt idle.
idle_interrupt_sender: Sender<bool>,
}
impl ConnectionState {
/// Shutdown this connection completely.
async fn stop(&self) {
// Trigger shutdown of the run loop.
self.stop_sender.send(()).await;
// Wait for a notification that the run loop has been shutdown.
self.shutdown_receiver.recv().await.ok();
}
async fn interrupt(&self, probe_network: bool) {
// Use try_send to avoid blocking on interrupts.
self.idle_interrupt_sender.try_send(probe_network).ok();
}
}
#[derive(Debug)]
pub(crate) struct SmtpConnectionState {
state: ConnectionState,
}
impl SmtpConnectionState {
fn new() -> (Self, SmtpConnectionHandlers) {
let (stop_sender, stop_receiver) = channel(1);
let (shutdown_sender, shutdown_receiver) = channel(1);
let (idle_interrupt_sender, idle_interrupt_receiver) = channel(1);
let handlers = SmtpConnectionHandlers {
connection: Smtp::new(),
stop_receiver,
shutdown_sender,
idle_interrupt_receiver,
};
let state = ConnectionState {
idle_interrupt_sender,
shutdown_receiver,
stop_sender,
};
let conn = SmtpConnectionState { state };
(conn, handlers)
}
/// Interrupt any form of idle.
async fn interrupt(&self, probe_network: bool) {
self.state.interrupt(probe_network).await;
}
/// Shutdown this connection completely.
async fn stop(&self) {
self.state.stop().await;
}
}
#[derive(Debug)]
struct SmtpConnectionHandlers {
connection: Smtp,
stop_receiver: Receiver<()>,
shutdown_sender: Sender<()>,
idle_interrupt_receiver: Receiver<bool>,
}
#[derive(Debug)]
pub(crate) struct ImapConnectionState {
state: ConnectionState,
}
impl ImapConnectionState {
/// Construct a new connection.
fn new() -> (Self, ImapConnectionHandlers) {
let (stop_sender, stop_receiver) = channel(1);
let (shutdown_sender, shutdown_receiver) = channel(1);
let (idle_interrupt_sender, idle_interrupt_receiver) = channel(1);
let handlers = ImapConnectionHandlers {
connection: Imap::new(idle_interrupt_receiver),
stop_receiver,
shutdown_sender,
};
let state = ConnectionState {
idle_interrupt_sender,
shutdown_receiver,
stop_sender,
};
let conn = ImapConnectionState { state };
(conn, handlers)
}
/// Interrupt any form of idle.
async fn interrupt(&self, probe_network: bool) {
self.state.interrupt(probe_network).await;
}
/// Shutdown this connection completely.
async fn stop(&self) {
self.state.stop().await;
}
}
#[derive(Debug)]
struct ImapConnectionHandlers {
connection: Imap,
stop_receiver: Receiver<()>,
shutdown_sender: Sender<()>,
}
async fn get_watch_folder(context: &Context, config_name: impl AsRef<str>) -> Option<String> {
match context
.sql
.get_raw_config(context, config_name.as_ref())
.await
{
Some(name) => Some(name),
None => {
if config_name.as_ref() == "configured_inbox_folder" {
// initialized with old version, so has not set configured_inbox_folder
Some("INBOX".to_string())
} else {
None
}
}
}
}

View File

@@ -1,5 +1,7 @@
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol) //! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol)
use std::time::Duration;
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference; use crate::aheader::EncryptPreference;
@@ -30,7 +32,7 @@ macro_rules! joiner_progress {
$progress >= 0 && $progress <= 1000, $progress >= 0 && $progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success" "value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
); );
$context.call_cb($crate::events::Event::SecurejoinJoinerProgress { $context.emit_event($crate::events::Event::SecurejoinJoinerProgress {
contact_id: $contact_id, contact_id: $contact_id,
progress: $progress, progress: $progress,
}); });
@@ -43,7 +45,7 @@ macro_rules! inviter_progress {
$progress >= 0 && $progress <= 1000, $progress >= 0 && $progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success" "value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
); );
$context.call_cb($crate::events::Event::SecurejoinInviterProgress { $context.emit_event($crate::events::Event::SecurejoinInviterProgress {
contact_id: $contact_id, contact_id: $contact_id,
progress: $progress, progress: $progress,
}); });
@@ -55,7 +57,7 @@ macro_rules! get_qr_attr {
$context $context
.bob .bob
.read() .read()
.unwrap() .await
.qr_scan .qr_scan
.as_ref() .as_ref()
.unwrap() .unwrap()
@@ -65,7 +67,7 @@ macro_rules! get_qr_attr {
}; };
} }
pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<String> { pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<String> {
/*======================================================= /*=======================================================
==== Alice - the inviter side ==== ==== Alice - the inviter side ====
==== Step 1 in "Setup verified contact" protocol ==== ==== Step 1 in "Setup verified contact" protocol ====
@@ -73,13 +75,14 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<
let fingerprint: String; let fingerprint: String;
ensure_secret_key_exists(context).ok(); ensure_secret_key_exists(context).await.ok();
// invitenumber will be used to allow starting the handshake, // invitenumber will be used to allow starting the handshake,
// auth will be used to verify the fingerprint // auth will be used to verify the fingerprint
let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group_chat_id); let invitenumber =
let auth = token::lookup_or_new(context, token::Namespace::Auth, group_chat_id); token::lookup_or_new(context, token::Namespace::InviteNumber, group_chat_id).await;
let self_addr = match context.get_config(Config::ConfiguredAddr) { let auth = token::lookup_or_new(context, token::Namespace::Auth, group_chat_id).await;
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
Some(addr) => addr, Some(addr) => addr,
None => { None => {
error!(context, "Not configured, cannot generate QR code.",); error!(context, "Not configured, cannot generate QR code.",);
@@ -87,9 +90,12 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<
} }
}; };
let self_name = context.get_config(Config::Displayname).unwrap_or_default(); let self_name = context
.get_config(Config::Displayname)
.await
.unwrap_or_default();
fingerprint = match get_self_fingerprint(context) { fingerprint = match get_self_fingerprint(context).await {
Some(fp) => fp, Some(fp) => fp,
None => { None => {
return None; return None;
@@ -103,7 +109,7 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<
let qr = if !group_chat_id.is_unset() { let qr = if !group_chat_id.is_unset() {
// parameters used: a=g=x=i=s= // parameters used: a=g=x=i=s=
if let Ok(chat) = Chat::load_from_db(context, group_chat_id) { if let Ok(chat) = Chat::load_from_db(context, group_chat_id).await {
let group_name = chat.get_name(); let group_name = chat.get_name();
let group_name_urlencoded = let group_name_urlencoded =
utf8_percent_encode(&group_name, NON_ALPHANUMERIC).to_string(); utf8_percent_encode(&group_name, NON_ALPHANUMERIC).to_string();
@@ -134,8 +140,8 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<
qr qr
} }
fn get_self_fingerprint(context: &Context) -> Option<String> { async fn get_self_fingerprint(context: &Context) -> Option<String> {
match SignedPublicKey::load_self(context) { match SignedPublicKey::load_self(context).await {
Ok(key) => Some(Key::from(key).fingerprint()), Ok(key) => Some(Key::from(key).fingerprint()),
Err(_) => { Err(_) => {
warn!(context, "get_self_fingerprint(): failed to load key"); warn!(context, "get_self_fingerprint(): failed to load key");
@@ -144,35 +150,48 @@ fn get_self_fingerprint(context: &Context) -> Option<String> {
} }
} }
async fn cleanup(
context: &Context,
contact_chat_id: ChatId,
ongoing_allocated: bool,
join_vg: bool,
) -> ChatId {
let mut bob = context.bob.write().await;
bob.expects = 0;
let ret_chat_id: ChatId = if bob.status == DC_BOB_SUCCESS {
if join_vg {
chat::get_chat_id_by_grpid(
context,
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
)
.await
.unwrap_or((ChatId::new(0), false, Blocked::Not))
.0
} else {
contact_chat_id
}
} else {
ChatId::new(0)
};
bob.qr_scan = None;
if ongoing_allocated {
context.free_ongoing().await;
}
ret_chat_id
}
/// Take a scanned QR-code and do the setup-contact/join-group handshake. /// Take a scanned QR-code and do the setup-contact/join-group handshake.
/// See the ffi-documentation for more details. /// See the ffi-documentation for more details.
pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId { pub async fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
let cleanup = if context.alloc_ongoing().await.is_err() {
|context: &Context, contact_chat_id: ChatId, ongoing_allocated: bool, join_vg: bool| { return cleanup(&context, ChatId::new(0), false, false).await;
let mut bob = context.bob.write().unwrap(); }
bob.expects = 0;
let ret_chat_id: ChatId = if bob.status == DC_BOB_SUCCESS {
if join_vg {
chat::get_chat_id_by_grpid(
context,
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
)
.unwrap_or((ChatId::new(0), false, Blocked::Not))
.0
} else {
contact_chat_id
}
} else {
ChatId::new(0)
};
bob.qr_scan = None;
if ongoing_allocated { securejoin(context, qr).await
context.free_ongoing(); }
}
ret_chat_id
};
async fn securejoin(context: &Context, qr: &str) -> ChatId {
/*======================================================== /*========================================================
==== Bob - the joiner's side ===== ==== Bob - the joiner's side =====
==== Step 2 in "Setup verified contact" protocol ===== ==== Step 2 in "Setup verified contact" protocol =====
@@ -182,29 +201,26 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
let mut join_vg: bool = false; let mut join_vg: bool = false;
info!(context, "Requesting secure-join ...",); info!(context, "Requesting secure-join ...",);
ensure_secret_key_exists(context).ok(); ensure_secret_key_exists(context).await.ok();
if !context.alloc_ongoing() { let qr_scan = check_qr(context, &qr).await;
return cleanup(&context, contact_chat_id, false, join_vg);
}
let qr_scan = check_qr(context, &qr);
if qr_scan.state != LotState::QrAskVerifyContact && qr_scan.state != LotState::QrAskVerifyGroup if qr_scan.state != LotState::QrAskVerifyContact && qr_scan.state != LotState::QrAskVerifyGroup
{ {
error!(context, "Unknown QR code.",); error!(context, "Unknown QR code.",);
return cleanup(&context, contact_chat_id, true, join_vg); return cleanup(&context, contact_chat_id, true, join_vg).await;
} }
contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id) { contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id).await {
Ok(chat_id) => chat_id, Ok(chat_id) => chat_id,
Err(_) => { Err(_) => {
error!(context, "Unknown contact."); error!(context, "Unknown contact.");
return cleanup(&context, contact_chat_id, true, join_vg); return cleanup(&context, contact_chat_id, true, join_vg).await;
} }
}; };
if context.shall_stop_ongoing() { if context.shall_stop_ongoing().await {
return cleanup(&context, contact_chat_id, true, join_vg); return cleanup(&context, contact_chat_id, true, join_vg).await;
} }
join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup; join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
{ {
let mut bob = context.bob.write().unwrap(); let mut bob = context.bob.write().await;
bob.status = 0; bob.status = 0;
bob.qr_scan = Some(qr_scan); bob.qr_scan = Some(qr_scan);
} }
@@ -213,7 +229,7 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
context context
.bob .bob
.read() .read()
.unwrap() .await
.qr_scan .qr_scan
.as_ref() .as_ref()
.unwrap() .unwrap()
@@ -221,16 +237,22 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
.as_ref() .as_ref()
.unwrap(), .unwrap(),
contact_chat_id, contact_chat_id,
) { )
.await
{
// the scanned fingerprint matches Alice's key, // the scanned fingerprint matches Alice's key,
// we can proceed to step 4b) directly and save two mails // we can proceed to step 4b) directly and save two mails
info!(context, "Taking protocol shortcut."); info!(context, "Taking protocol shortcut.");
context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM; context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM;
joiner_progress!(context, chat_id_2_contact_id(context, contact_chat_id), 400); joiner_progress!(
let own_fingerprint = get_self_fingerprint(context).unwrap_or_default(); context,
chat_id_2_contact_id(context, contact_chat_id).await,
400
);
let own_fingerprint = get_self_fingerprint(context).await.unwrap_or_default();
// Bob -> Alice // Bob -> Alice
send_handshake_msg( if let Err(err) = send_handshake_msg(
context, context,
contact_chat_id, contact_chat_id,
if join_vg { if join_vg {
@@ -245,43 +267,53 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
} else { } else {
"".to_string() "".to_string()
}, },
); )
.await
{
error!(context, "failed to send handshake message: {}", err);
return cleanup(&context, contact_chat_id, true, join_vg).await;
}
} else { } else {
context.bob.write().unwrap().expects = DC_VC_AUTH_REQUIRED; context.bob.write().await.expects = DC_VC_AUTH_REQUIRED;
// Bob -> Alice // Bob -> Alice
send_handshake_msg( if let Err(err) = send_handshake_msg(
context, context,
contact_chat_id, contact_chat_id,
if join_vg { "vg-request" } else { "vc-request" }, if join_vg { "vg-request" } else { "vc-request" },
get_qr_attr!(context, invitenumber), get_qr_attr!(context, invitenumber),
None, None,
"", "",
); )
.await
{
error!(context, "failed to send handshake message: {}", err);
return cleanup(&context, contact_chat_id, true, join_vg).await;
}
} }
if join_vg { if join_vg {
// for a group-join, wait until the secure-join is done and the group is created // for a group-join, wait until the secure-join is done and the group is created
while !context.shall_stop_ongoing() { while !context.shall_stop_ongoing().await {
std::thread::sleep(std::time::Duration::from_millis(200)); async_std::task::sleep(Duration::from_millis(50)).await;
} }
cleanup(&context, contact_chat_id, true, join_vg) cleanup(&context, contact_chat_id, true, join_vg).await
} else { } else {
// for a one-to-one-chat, the chat is already known, return the chat-id, // for a one-to-one-chat, the chat is already known, return the chat-id,
// the verification runs in background // the verification runs in background
context.free_ongoing(); context.free_ongoing().await;
contact_chat_id contact_chat_id
} }
} }
fn send_handshake_msg( async fn send_handshake_msg(
context: &Context, context: &Context,
contact_chat_id: ChatId, contact_chat_id: ChatId,
step: &str, step: &str,
param2: impl AsRef<str>, param2: impl AsRef<str>,
fingerprint: Option<String>, fingerprint: Option<String>,
grpid: impl AsRef<str>, grpid: impl AsRef<str>,
) { ) -> Result<(), HandshakeError> {
let mut msg = Message::default(); let mut msg = Message::default();
msg.viewtype = Viewtype::Text; msg.viewtype = Viewtype::Text;
msg.text = Some(format!("Secure-Join: {}", step)); msg.text = Some(format!("Secure-Join: {}", step));
@@ -309,12 +341,16 @@ fn send_handshake_msg(
} else { } else {
msg.param.set_int(Param::GuaranteeE2ee, 1); msg.param.set_int(Param::GuaranteeE2ee, 1);
} }
// TODO. handle cleanup on error
chat::send_msg(context, contact_chat_id, &mut msg).unwrap_or_default(); chat::send_msg(context, contact_chat_id, &mut msg)
.await
.map_err(HandshakeError::MsgSendFailed)?;
Ok(())
} }
fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 { async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
let contacts = chat::get_chat_contacts(context, contact_chat_id); let contacts = chat::get_chat_contacts(context, contact_chat_id).await;
if contacts.len() == 1 { if contacts.len() == 1 {
contacts[0] contacts[0]
} else { } else {
@@ -322,17 +358,16 @@ fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
} }
} }
fn fingerprint_equals_sender( async fn fingerprint_equals_sender(
context: &Context, context: &Context,
fingerprint: impl AsRef<str>, fingerprint: impl AsRef<str>,
contact_chat_id: ChatId, contact_chat_id: ChatId,
) -> bool { ) -> bool {
let contacts = chat::get_chat_contacts(context, contact_chat_id); let contacts = chat::get_chat_contacts(context, contact_chat_id).await;
if contacts.len() == 1 { if contacts.len() == 1 {
if let Ok(contact) = Contact::load_from_db(context, contacts[0]) { if let Ok(contact) = Contact::load_from_db(context, contacts[0]).await {
if let Some(peerstate) = Peerstate::from_addr(context, &context.sql, contact.get_addr()) if let Some(peerstate) = Peerstate::from_addr(context, contact.get_addr()).await {
{
let fingerprint_normalized = dc_normalize_fingerprint(fingerprint.as_ref()); let fingerprint_normalized = dc_normalize_fingerprint(fingerprint.as_ref());
if peerstate.public_key_fingerprint.is_some() if peerstate.public_key_fingerprint.is_some()
&& &fingerprint_normalized == peerstate.public_key_fingerprint.as_ref().unwrap() && &fingerprint_normalized == peerstate.public_key_fingerprint.as_ref().unwrap()
@@ -360,6 +395,8 @@ pub(crate) enum HandshakeError {
ChatNotFound { group: String }, ChatNotFound { group: String },
#[error("No configured self address found")] #[error("No configured self address found")]
NoSelfAddr, NoSelfAddr,
#[error("Failed to send message")]
MsgSendFailed(#[source] Error),
} }
/// What to do with a Secure-Join handshake message after it was handled. /// What to do with a Secure-Join handshake message after it was handled.
@@ -386,7 +423,7 @@ pub(crate) enum HandshakeMessage {
/// When handle_securejoin_handshake() is called, /// When handle_securejoin_handshake() is called,
/// the message is not yet filed in the database; /// the message is not yet filed in the database;
/// this is done by receive_imf() later on as needed. /// this is done by receive_imf() later on as needed.
pub(crate) fn handle_securejoin_handshake( pub(crate) async fn handle_securejoin_handshake(
context: &Context, context: &Context,
mime_message: &MimeMessage, mime_message: &MimeMessage,
contact_id: u32, contact_id: u32,
@@ -404,10 +441,10 @@ pub(crate) fn handle_securejoin_handshake(
); );
let contact_chat_id = let contact_chat_id =
match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not) { match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not).await {
Ok((chat_id, blocked)) => { Ok((chat_id, blocked)) => {
if blocked != Blocked::Not { if blocked != Blocked::Not {
chat_id.unblock(context); chat_id.unblock(context).await;
} }
chat_id chat_id
} }
@@ -439,7 +476,7 @@ pub(crate) fn handle_securejoin_handshake(
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
}; };
if !token::exists(context, token::Namespace::InviteNumber, &invitenumber) { if !token::exists(context, token::Namespace::InviteNumber, &invitenumber).await {
warn!(context, "Secure-join denied (bad invitenumber)."); warn!(context, "Secure-join denied (bad invitenumber).");
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
@@ -455,7 +492,8 @@ pub(crate) fn handle_securejoin_handshake(
"", "",
None, None,
"", "",
); )
.await?;
Ok(HandshakeMessage::Done) Ok(HandshakeMessage::Done)
} }
"vg-auth-required" | "vc-auth-required" => { "vg-auth-required" | "vc-auth-required" => {
@@ -466,7 +504,7 @@ pub(crate) fn handle_securejoin_handshake(
// verify that Alice's Autocrypt key and fingerprint matches the QR-code // verify that Alice's Autocrypt key and fingerprint matches the QR-code
let cond = { let cond = {
let bob = context.bob.read().unwrap(); let bob = context.bob.read().await;
let scan = bob.qr_scan.as_ref(); let scan = bob.qr_scan.as_ref();
scan.is_none() scan.is_none()
|| bob.expects != DC_VC_AUTH_REQUIRED || bob.expects != DC_VC_AUTH_REQUIRED
@@ -490,25 +528,29 @@ pub(crate) fn handle_securejoin_handshake(
} else { } else {
"Not encrypted." "Not encrypted."
}, },
); )
context.bob.write().unwrap().status = 0; // secure-join failed .await;
context.stop_ongoing(); context.bob.write().await.status = 0; // secure-join failed
context.stop_ongoing().await;
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id) { if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id)
.await
{
could_not_establish_secure_connection( could_not_establish_secure_connection(
context, context,
contact_chat_id, contact_chat_id,
"Fingerprint mismatch on joiner-side.", "Fingerprint mismatch on joiner-side.",
); )
context.bob.write().unwrap().status = 0; // secure-join failed .await;
context.stop_ongoing(); context.bob.write().await.status = 0; // secure-join failed
context.stop_ongoing().await;
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
info!(context, "Fingerprint verified.",); info!(context, "Fingerprint verified.",);
let own_fingerprint = get_self_fingerprint(context).unwrap(); let own_fingerprint = get_self_fingerprint(context).await.unwrap();
joiner_progress!(context, contact_id, 400); joiner_progress!(context, contact_id, 400);
context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM; context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM;
// Bob -> Alice // Bob -> Alice
send_handshake_msg( send_handshake_msg(
@@ -522,7 +564,8 @@ pub(crate) fn handle_securejoin_handshake(
} else { } else {
"".to_string() "".to_string()
}, },
); )
.await?;
Ok(HandshakeMessage::Done) Ok(HandshakeMessage::Done)
} }
"vg-request-with-auth" | "vc-request-with-auth" => { "vg-request-with-auth" | "vc-request-with-auth" => {
@@ -540,7 +583,8 @@ pub(crate) fn handle_securejoin_handshake(
context, context,
contact_chat_id, contact_chat_id,
"Fingerprint not provided.", "Fingerprint not provided.",
); )
.await;
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
}; };
@@ -549,15 +593,17 @@ pub(crate) fn handle_securejoin_handshake(
context, context,
contact_chat_id, contact_chat_id,
"Auth not encrypted.", "Auth not encrypted.",
); )
.await;
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id) { if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await {
could_not_establish_secure_connection( could_not_establish_secure_connection(
context, context,
contact_chat_id, contact_chat_id,
"Fingerprint mismatch on inviter-side.", "Fingerprint mismatch on inviter-side.",
); )
.await;
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
info!(context, "Fingerprint verified.",); info!(context, "Fingerprint verified.",);
@@ -569,25 +615,28 @@ pub(crate) fn handle_securejoin_handshake(
context, context,
contact_chat_id, contact_chat_id,
"Auth not provided.", "Auth not provided.",
); )
.await;
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
}; };
if !token::exists(context, token::Namespace::Auth, &auth_0) { if !token::exists(context, token::Namespace::Auth, &auth_0).await {
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid."); could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.")
.await;
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
if mark_peer_as_verified(context, fingerprint).is_err() { if mark_peer_as_verified(context, fingerprint).await.is_err() {
could_not_establish_secure_connection( could_not_establish_secure_connection(
context, context,
contact_chat_id, contact_chat_id,
"Fingerprint mismatch on inviter-side.", "Fingerprint mismatch on inviter-side.",
); )
.await;
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited); Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await;
info!(context, "Auth verified.",); info!(context, "Auth verified.",);
secure_connection_established(context, contact_chat_id); secure_connection_established(context, contact_chat_id).await;
emit_event!(context, Event::ContactsChanged(Some(contact_id))); emit_event!(context, Event::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600); inviter_progress!(context, contact_id, 600);
if join_vg { if join_vg {
@@ -601,10 +650,11 @@ pub(crate) fn handle_securejoin_handshake(
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
}; };
match chat::get_chat_id_by_grpid(context, field_grpid) { match chat::get_chat_id_by_grpid(context, field_grpid).await {
Ok((group_chat_id, _, _)) => { Ok((group_chat_id, _, _)) => {
if let Err(err) = if let Err(err) =
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true) chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
.await
{ {
error!(context, "failed to add contact: {}", err); error!(context, "failed to add contact: {}", err);
} }
@@ -625,7 +675,9 @@ pub(crate) fn handle_securejoin_handshake(
"", "",
Some(fingerprint.clone()), Some(fingerprint.clone()),
"", "",
); )
.await?;
inviter_progress!(context, contact_id, 1000); inviter_progress!(context, contact_id, 1000);
} }
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed) Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
@@ -641,12 +693,12 @@ pub(crate) fn handle_securejoin_handshake(
HandshakeMessage::Ignore HandshakeMessage::Ignore
}; };
if context.bob.read().unwrap().expects != DC_VC_CONTACT_CONFIRM { if context.bob.read().await.expects != DC_VC_CONTACT_CONFIRM {
info!(context, "Message belongs to a different handshake.",); info!(context, "Message belongs to a different handshake.",);
return Ok(abort_retval); return Ok(abort_retval);
} }
let cond = { let cond = {
let bob = context.bob.read().unwrap(); let bob = context.bob.read().await;
let scan = bob.qr_scan.as_ref(); let scan = bob.qr_scan.as_ref();
scan.is_none() || (join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup) scan.is_none() || (join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup)
}; };
@@ -667,6 +719,7 @@ pub(crate) fn handle_securejoin_handshake(
// only after we have returned. It does not impact // only after we have returned. It does not impact
// the security invariants of secure-join however. // the security invariants of secure-join however.
let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, &group_id) let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, &group_id)
.await
.unwrap_or((ChatId::new(0), false, Blocked::Not)); .unwrap_or((ChatId::new(0), false, Blocked::Not));
// when joining a non-verified group // when joining a non-verified group
// the vg-member-added message may be unencrypted // the vg-member-added message may be unencrypted
@@ -684,20 +737,25 @@ pub(crate) fn handle_securejoin_handshake(
context, context,
contact_chat_id, contact_chat_id,
"Contact confirm message not encrypted.", "Contact confirm message not encrypted.",
); )
context.bob.write().unwrap().status = 0; .await;
context.bob.write().await.status = 0;
return Ok(abort_retval); return Ok(abort_retval);
} }
if mark_peer_as_verified(context, &scanned_fingerprint_of_alice).is_err() { if mark_peer_as_verified(context, &scanned_fingerprint_of_alice)
.await
.is_err()
{
could_not_establish_secure_connection( could_not_establish_secure_connection(
context, context,
contact_chat_id, contact_chat_id,
"Fingerprint mismatch on joiner-side.", "Fingerprint mismatch on joiner-side.",
); )
.await;
return Ok(abort_retval); return Ok(abort_retval);
} }
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined); Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined).await;
emit_event!(context, Event::ContactsChanged(None)); emit_event!(context, Event::ContactsChanged(None));
let cg_member_added = mime_message let cg_member_added = mime_message
.get(HeaderDef::ChatGroupMemberAdded) .get(HeaderDef::ChatGroupMemberAdded)
@@ -706,13 +764,14 @@ pub(crate) fn handle_securejoin_handshake(
if join_vg if join_vg
&& !context && !context
.is_self_addr(cg_member_added) .is_self_addr(cg_member_added)
.await
.map_err(|_| HandshakeError::NoSelfAddr)? .map_err(|_| HandshakeError::NoSelfAddr)?
{ {
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group)."); info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
return Ok(abort_retval); return Ok(abort_retval);
} }
secure_connection_established(context, contact_chat_id); secure_connection_established(context, contact_chat_id).await;
context.bob.write().unwrap().expects = 0; context.bob.write().await.expects = 0;
// Bob -> Alice // Bob -> Alice
send_handshake_msg( send_handshake_msg(
@@ -726,10 +785,11 @@ pub(crate) fn handle_securejoin_handshake(
"", "",
Some(scanned_fingerprint_of_alice), Some(scanned_fingerprint_of_alice),
"", "",
); )
.await?;
context.bob.write().unwrap().status = 1; context.bob.write().await.status = 1;
context.stop_ongoing(); context.stop_ongoing().await;
Ok(if join_vg { Ok(if join_vg {
HandshakeMessage::Propagate HandshakeMessage::Propagate
} else { } else {
@@ -742,8 +802,8 @@ pub(crate) fn handle_securejoin_handshake(
==== Step 8 in "Out-of-band verified groups" protocol ==== ==== Step 8 in "Out-of-band verified groups" protocol ====
==========================================================*/ ==========================================================*/
if let Ok(contact) = Contact::get_by_id(context, contact_id) { if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
if contact.is_verified(context) == VerifiedStatus::Unverified { if contact.is_verified(context).await == VerifiedStatus::Unverified {
warn!(context, "{} invalid.", step); warn!(context, "{} invalid.", step);
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
@@ -754,7 +814,7 @@ pub(crate) fn handle_securejoin_handshake(
.get(HeaderDef::SecureJoinGroup) .get(HeaderDef::SecureJoinGroup)
.map(|s| s.as_str()) .map(|s| s.as_str())
.unwrap_or_else(|| ""); .unwrap_or_else(|| "");
if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid) { if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid).await {
warn!(context, "Failed to lookup chat_id from grpid: {}", err); warn!(context, "Failed to lookup chat_id from grpid: {}", err);
return Err(HandshakeError::ChatNotFound { return Err(HandshakeError::ChatNotFound {
group: field_grpid.to_string(), group: field_grpid.to_string(),
@@ -791,7 +851,7 @@ pub(crate) fn handle_securejoin_handshake(
/// the joining device has marked the peer as verified on vg-member-added/vc-contact-confirm /// the joining device has marked the peer as verified on vg-member-added/vc-contact-confirm
/// before sending vg-member-added-received - so, if we observe vg-member-added-received, /// before sending vg-member-added-received - so, if we observe vg-member-added-received,
/// we can mark the peer as verified as well. /// we can mark the peer as verified as well.
pub(crate) fn observe_securejoin_on_other_device( pub(crate) async fn observe_securejoin_on_other_device(
context: &Context, context: &Context,
mime_message: &MimeMessage, mime_message: &MimeMessage,
contact_id: u32, contact_id: u32,
@@ -805,10 +865,10 @@ pub(crate) fn observe_securejoin_on_other_device(
info!(context, "observing secure-join message \'{}\'", step); info!(context, "observing secure-join message \'{}\'", step);
let contact_chat_id = let contact_chat_id =
match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not) { match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not).await {
Ok((chat_id, blocked)) => { Ok((chat_id, blocked)) => {
if blocked != Blocked::Not { if blocked != Blocked::Not {
chat_id.unblock(context); chat_id.unblock(context).await;
} }
chat_id chat_id
} }
@@ -828,13 +888,14 @@ pub(crate) fn observe_securejoin_on_other_device(
if !encrypted_and_signed( if !encrypted_and_signed(
context, context,
mime_message, mime_message,
get_self_fingerprint(context).unwrap_or_default(), get_self_fingerprint(context).await.unwrap_or_default(),
) { ) {
could_not_establish_secure_connection( could_not_establish_secure_connection(
context, context,
contact_chat_id, contact_chat_id,
"Message not encrypted correctly.", "Message not encrypted correctly.",
); )
.await;
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
let fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint) { let fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint) {
@@ -844,16 +905,18 @@ pub(crate) fn observe_securejoin_on_other_device(
context, context,
contact_chat_id, contact_chat_id,
"Fingerprint not provided, please update Delta Chat on all your devices.", "Fingerprint not provided, please update Delta Chat on all your devices.",
); )
.await;
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
}; };
if mark_peer_as_verified(context, fingerprint).is_err() { if mark_peer_as_verified(context, fingerprint).await.is_err() {
could_not_establish_secure_connection( could_not_establish_secure_connection(
context, context,
contact_chat_id, contact_chat_id,
format!("Fingerprint mismatch on observing {}.", step).as_ref(), format!("Fingerprint mismatch on observing {}.", step).as_ref(),
); )
.await;
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
} }
Ok(if step.as_str() == "vg-member-added" { Ok(if step.as_str() == "vg-member-added" {
@@ -866,42 +929,50 @@ pub(crate) fn observe_securejoin_on_other_device(
} }
} }
fn secure_connection_established(context: &Context, contact_chat_id: ChatId) { async fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id); let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id).await;
let contact = Contact::get_by_id(context, contact_id); let contact = Contact::get_by_id(context, contact_id).await;
let addr = if let Ok(ref contact) = contact { let addr = if let Ok(ref contact) = contact {
contact.get_addr() contact.get_addr()
} else { } else {
"?" "?"
}; };
let msg = context.stock_string_repl_str(StockMessage::ContactVerified, addr); let msg = context
chat::add_info_msg(context, contact_chat_id, msg); .stock_string_repl_str(StockMessage::ContactVerified, addr)
.await;
chat::add_info_msg(context, contact_chat_id, msg).await;
emit_event!(context, Event::ChatModified(contact_chat_id)); emit_event!(context, Event::ChatModified(contact_chat_id));
} }
fn could_not_establish_secure_connection( async fn could_not_establish_secure_connection(
context: &Context, context: &Context,
contact_chat_id: ChatId, contact_chat_id: ChatId,
details: &str, details: &str,
) { ) {
let contact_id = chat_id_2_contact_id(context, contact_chat_id); let contact_id = chat_id_2_contact_id(context, contact_chat_id).await;
let contact = Contact::get_by_id(context, contact_id); let contact = Contact::get_by_id(context, contact_id).await;
let msg = context.stock_string_repl_str( let msg = context
StockMessage::ContactNotVerified, .stock_string_repl_str(
if let Ok(ref contact) = contact { StockMessage::ContactNotVerified,
contact.get_addr() if let Ok(ref contact) = contact {
} else { contact.get_addr()
"?" } else {
}, "?"
); },
)
.await;
chat::add_info_msg(context, contact_chat_id, &msg); chat::add_info_msg(context, contact_chat_id, &msg).await;
error!(context, "{} ({})", &msg, details); error!(context, "{} ({})", &msg, details);
} }
fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Result<(), Error> { async fn mark_peer_as_verified(
context: &Context,
fingerprint: impl AsRef<str>,
) -> Result<(), Error> {
if let Some(ref mut peerstate) = if let Some(ref mut peerstate) =
Peerstate::from_fingerprint(context, &context.sql, fingerprint.as_ref()) Peerstate::from_fingerprint(context, &context.sql, fingerprint.as_ref()).await
{ {
if peerstate.set_verified( if peerstate.set_verified(
PeerstateKeyType::PublicKey, PeerstateKeyType::PublicKey,
@@ -912,6 +983,7 @@ fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Res
peerstate.to_save = Some(ToSave::All); peerstate.to_save = Some(ToSave::All);
peerstate peerstate
.save_to_db(&context.sql, false) .save_to_db(&context.sql, false)
.await
.unwrap_or_default(); .unwrap_or_default();
return Ok(()); return Ok(());
} }
@@ -955,18 +1027,25 @@ fn encrypted_and_signed(
} }
} }
pub fn handle_degrade_event(context: &Context, peerstate: &Peerstate) -> Result<(), Error> { pub async fn handle_degrade_event(
context: &Context,
peerstate: &Peerstate<'_>,
) -> Result<(), Error> {
// - we do not issue an warning for DC_DE_ENCRYPTION_PAUSED as this is quite normal // - we do not issue an warning for DC_DE_ENCRYPTION_PAUSED as this is quite normal
// - currently, we do not issue an extra warning for DC_DE_VERIFICATION_LOST - this always comes // - currently, we do not issue an extra warning for DC_DE_VERIFICATION_LOST - this always comes
// together with DC_DE_FINGERPRINT_CHANGED which is logged, the idea is not to bother // together with DC_DE_FINGERPRINT_CHANGED which is logged, the idea is not to bother
// with things they cannot fix, so the user is just kicked from the verified group // with things they cannot fix, so the user is just kicked from the verified group
// (and he will know this and can fix this) // (and he will know this and can fix this)
if Some(DegradeEvent::FingerprintChanged) == peerstate.degrade_event { if Some(DegradeEvent::FingerprintChanged) == peerstate.degrade_event {
let contact_id: i32 = match context.sql.query_get_value( let contact_id: i32 = match context
context, .sql
"SELECT id FROM contacts WHERE addr=?;", .query_get_value(
params![&peerstate.addr], context,
) { "SELECT id FROM contacts WHERE addr=?;",
paramsv![peerstate.addr],
)
.await
{
None => bail!( None => bail!(
"contact with peerstate.addr {:?} not found", "contact with peerstate.addr {:?} not found",
&peerstate.addr &peerstate.addr
@@ -976,12 +1055,14 @@ pub fn handle_degrade_event(context: &Context, peerstate: &Peerstate) -> Result<
if contact_id > 0 { if contact_id > 0 {
let (contact_chat_id, _) = let (contact_chat_id, _) =
chat::create_or_lookup_by_contact_id(context, contact_id as u32, Blocked::Deaddrop) chat::create_or_lookup_by_contact_id(context, contact_id as u32, Blocked::Deaddrop)
.await
.unwrap_or_default(); .unwrap_or_default();
let msg = context let msg = context
.stock_string_repl_str(StockMessage::ContactSetupChanged, peerstate.addr.clone()); .stock_string_repl_str(StockMessage::ContactSetupChanged, peerstate.addr.clone())
.await;
chat::add_info_msg(context, contact_chat_id, msg); chat::add_info_msg(context, contact_chat_id, msg).await;
emit_event!(context, Event::ChatModified(contact_chat_id)); emit_event!(context, Event::ChatModified(contact_chat_id));
} }
} }

View File

@@ -65,16 +65,16 @@ impl Smtp {
} }
/// Disconnect the SMTP transport and drop it entirely. /// Disconnect the SMTP transport and drop it entirely.
pub fn disconnect(&mut self) { pub async fn disconnect(&mut self) {
if let Some(mut transport) = self.transport.take() { if let Some(mut transport) = self.transport.take() {
async_std::task::block_on(transport.close()).ok(); transport.close().await.ok();
} }
self.last_success = None; self.last_success = None;
} }
/// Return true if smtp was connected but is not known to /// Return true if smtp was connected but is not known to
/// have been successfully used the last 60 seconds /// have been successfully used the last 60 seconds
pub fn has_maybe_stale_connection(&self) -> bool { pub async fn has_maybe_stale_connection(&self) -> bool {
if let Some(last_success) = self.last_success { if let Some(last_success) = self.last_success {
Instant::now().duration_since(last_success).as_secs() > 60 Instant::now().duration_since(last_success).as_secs() > 60
} else { } else {
@@ -83,7 +83,7 @@ impl Smtp {
} }
/// Check whether we are connected. /// Check whether we are connected.
pub fn is_connected(&self) -> bool { pub async fn is_connected(&self) -> bool {
self.transport self.transport
.as_ref() .as_ref()
.map(|t| t.is_connected()) .map(|t| t.is_connected())
@@ -91,18 +91,14 @@ impl Smtp {
} }
/// Connect using the provided login params. /// Connect using the provided login params.
pub fn connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> { pub async fn connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> {
async_std::task::block_on(self.inner_connect(context, lp)) if self.is_connected().await {
}
async fn inner_connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> {
if self.is_connected() {
warn!(context, "SMTP already connected."); warn!(context, "SMTP already connected.");
return Ok(()); return Ok(());
} }
if lp.send_server.is_empty() || lp.send_port == 0 { if lp.send_server.is_empty() || lp.send_port == 0 {
context.call_cb(Event::ErrorNetwork("SMTP bad parameters.".into())); context.emit_event(Event::ErrorNetwork("SMTP bad parameters.".into()));
return Err(Error::BadParameters); return Err(Error::BadParameters);
} }
@@ -111,6 +107,7 @@ impl Smtp {
address: lp.addr.clone(), address: lp.addr.clone(),
error: err, error: err,
})?; })?;
self.from = Some(from); self.from = Some(from);
let domain = &lp.send_server; let domain = &lp.send_server;
@@ -123,7 +120,7 @@ impl Smtp {
// oauth2 // oauth2
let addr = &lp.addr; let addr = &lp.addr;
let send_pw = &lp.send_pw; let send_pw = &lp.send_pw;
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false); let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await;
if access_token.is_none() { if access_token.is_none() {
return Err(Error::Oauth2Error { return Err(Error::Oauth2Error {
address: addr.to_string(), address: addr.to_string(),
@@ -170,22 +167,23 @@ impl Smtp {
.timeout(Some(Duration::from_secs(SMTP_TIMEOUT))); .timeout(Some(Duration::from_secs(SMTP_TIMEOUT)));
let mut trans = client.into_transport(); let mut trans = client.into_transport();
if let Err(err) = trans.connect().await {
trans.connect().await.map_err(|err| { let message = context
let message = { .stock_string_repl_str2(
context.stock_string_repl_str2(
StockMessage::ServerResponse, StockMessage::ServerResponse,
format!("SMTP {}:{}", domain, port), format!("SMTP {}:{}", domain, port),
err.to_string(), err.to_string(),
) )
}; .await;
emit_event!(context, Event::ErrorNetwork(message)); emit_event!(context, Event::ErrorNetwork(message));
Error::ConnectionFailure(err) return Err(Error::ConnectionFailure(err));
})?; }
self.transport = Some(trans); self.transport = Some(trans);
self.last_success = Some(Instant::now()); self.last_success = Some(Instant::now());
context.call_cb(Event::SmtpConnected(format!(
context.emit_event(Event::SmtpConnected(format!(
"SMTP-LOGIN as {} ok", "SMTP-LOGIN as {} ok",
lp.send_user, lp.send_user,
))); )));

View File

@@ -49,7 +49,7 @@ impl Smtp {
if let Some(ref mut transport) = self.transport { if let Some(ref mut transport) = self.transport {
transport.send(mail).await.map_err(Error::SendError)?; transport.send(mail).await.map_err(Error::SendError)?;
context.call_cb(Event::SmtpMessageSent(format!( context.emit_event(Event::SmtpMessageSent(format!(
"Message len={} was smtp-sent to {}", "Message len={} was smtp-sent to {}",
message_len, recipients_display message_len, recipients_display
))); )));

1727
src/sql.rs

File diff suppressed because it is too large Load Diff

View File

@@ -197,7 +197,7 @@ impl StockMessage {
impl Context { impl Context {
/// Set the stock string for the [StockMessage]. /// Set the stock string for the [StockMessage].
/// ///
pub fn set_stock_translation( pub async fn set_stock_translation(
&self, &self,
id: StockMessage, id: StockMessage,
stockstring: String, stockstring: String,
@@ -218,7 +218,7 @@ impl Context {
} }
self.translated_stockstrings self.translated_stockstrings
.write() .write()
.unwrap() .await
.insert(id as usize, stockstring); .insert(id as usize, stockstring);
Ok(()) Ok(())
} }
@@ -227,11 +227,11 @@ impl Context {
/// ///
/// Return a translation (if it was set with set_stock_translation before) /// Return a translation (if it was set with set_stock_translation before)
/// or a default (English) string. /// or a default (English) string.
pub fn stock_str(&self, id: StockMessage) -> Cow<str> { pub async fn stock_str(&self, id: StockMessage) -> Cow<'_, str> {
match self match self
.translated_stockstrings .translated_stockstrings
.read() .read()
.unwrap() .await
.get(&(id as usize)) .get(&(id as usize))
{ {
Some(ref x) => Cow::Owned((*x).to_string()), Some(ref x) => Cow::Owned((*x).to_string()),
@@ -244,8 +244,9 @@ impl Context {
/// This replaces both the *first* `%1$s`, `%1$d` and `%1$@` /// This replaces both the *first* `%1$s`, `%1$d` and `%1$@`
/// placeholders with the provided string. /// placeholders with the provided string.
/// (the `%1$@` variant is used on iOS, the other are used on Android and Desktop) /// (the `%1$@` variant is used on iOS, the other are used on Android and Desktop)
pub fn stock_string_repl_str(&self, id: StockMessage, insert: impl AsRef<str>) -> String { pub async fn stock_string_repl_str(&self, id: StockMessage, insert: impl AsRef<str>) -> String {
self.stock_str(id) self.stock_str(id)
.await
.replacen("%1$s", insert.as_ref(), 1) .replacen("%1$s", insert.as_ref(), 1)
.replacen("%1$d", insert.as_ref(), 1) .replacen("%1$d", insert.as_ref(), 1)
.replacen("%1$@", insert.as_ref(), 1) .replacen("%1$@", insert.as_ref(), 1)
@@ -255,8 +256,9 @@ impl Context {
/// ///
/// Like [Context::stock_string_repl_str] but substitute the placeholders /// Like [Context::stock_string_repl_str] but substitute the placeholders
/// with an integer. /// with an integer.
pub fn stock_string_repl_int(&self, id: StockMessage, insert: i32) -> String { pub async fn stock_string_repl_int(&self, id: StockMessage, insert: i32) -> String {
self.stock_string_repl_str(id, format!("{}", insert).as_str()) self.stock_string_repl_str(id, format!("{}", insert).as_str())
.await
} }
/// Return stock string, replacing 2 placeholders with provided string. /// Return stock string, replacing 2 placeholders with provided string.
@@ -265,13 +267,14 @@ impl Context {
/// placeholders with the string in `insert` and does the same for /// placeholders with the string in `insert` and does the same for
/// `%2$s`, `%2$d` and `%2$@` for `insert2`. /// `%2$s`, `%2$d` and `%2$@` for `insert2`.
/// (the `%1$@` variant is used on iOS, the other are used on Android and Desktop) /// (the `%1$@` variant is used on iOS, the other are used on Android and Desktop)
pub fn stock_string_repl_str2( pub async fn stock_string_repl_str2(
&self, &self,
id: StockMessage, id: StockMessage,
insert: impl AsRef<str>, insert: impl AsRef<str>,
insert2: impl AsRef<str>, insert2: impl AsRef<str>,
) -> String { ) -> String {
self.stock_str(id) self.stock_str(id)
.await
.replacen("%1$s", insert.as_ref(), 1) .replacen("%1$s", insert.as_ref(), 1)
.replacen("%1$d", insert.as_ref(), 1) .replacen("%1$d", insert.as_ref(), 1)
.replacen("%1$@", insert.as_ref(), 1) .replacen("%1$@", insert.as_ref(), 1)
@@ -297,7 +300,7 @@ impl Context {
/// used as the second parameter to [StockMessage::MsgActionByUser] with /// used as the second parameter to [StockMessage::MsgActionByUser] with
/// again the original stock string being used as the first parameter, /// again the original stock string being used as the first parameter,
/// resulting in a string like "Member Alice added by Bob.". /// resulting in a string like "Member Alice added by Bob.".
pub fn stock_system_msg( pub async fn stock_system_msg(
&self, &self,
id: StockMessage, id: StockMessage,
param1: impl AsRef<str>, param1: impl AsRef<str>,
@@ -305,9 +308,11 @@ impl Context {
from_id: u32, from_id: u32,
) -> String { ) -> String {
let insert1 = if id == StockMessage::MsgAddMember || id == StockMessage::MsgDelMember { let insert1 = if id == StockMessage::MsgAddMember || id == StockMessage::MsgDelMember {
let contact_id = Contact::lookup_id_by_addr(self, param1.as_ref(), Origin::Unknown); let contact_id =
Contact::lookup_id_by_addr(self, param1.as_ref(), Origin::Unknown).await;
if contact_id != 0 { if contact_id != 0 {
Contact::get_by_id(self, contact_id) Contact::get_by_id(self, contact_id)
.await
.map(|contact| contact.get_name_n_addr()) .map(|contact| contact.get_name_n_addr())
.unwrap_or_default() .unwrap_or_default()
} else { } else {
@@ -317,52 +322,60 @@ impl Context {
param1.as_ref().to_string() param1.as_ref().to_string()
}; };
let action = self.stock_string_repl_str2(id, insert1, param2.as_ref().to_string()); let action = self
.stock_string_repl_str2(id, insert1, param2.as_ref().to_string())
.await;
let action1 = action.trim_end_matches('.'); let action1 = action.trim_end_matches('.');
match from_id { match from_id {
0 => action, 0 => action,
1 => self.stock_string_repl_str(StockMessage::MsgActionByMe, action1), // DC_CONTACT_ID_SELF 1 => {
self.stock_string_repl_str(StockMessage::MsgActionByMe, action1)
.await
} // DC_CONTACT_ID_SELF
_ => { _ => {
let displayname = Contact::get_by_id(self, from_id) let displayname = Contact::get_by_id(self, from_id)
.await
.map(|contact| contact.get_name_n_addr()) .map(|contact| contact.get_name_n_addr())
.unwrap_or_default(); .unwrap_or_default();
self.stock_string_repl_str2(StockMessage::MsgActionByUser, action1, &displayname) self.stock_string_repl_str2(StockMessage::MsgActionByUser, action1, &displayname)
.await
} }
} }
} }
pub fn update_device_chats(&self) -> Result<(), Error> { pub async fn update_device_chats(&self) -> Result<(), Error> {
// check for the LAST added device message - if it is present, we can skip message creation. // check for the LAST added device message - if it is present, we can skip message creation.
// this is worthwhile as this function is typically called // this is worthwhile as this function is typically called
// by the ui on every probram start or even on every opening of the chatlist. // by the ui on every probram start or even on every opening of the chatlist.
if chat::was_device_msg_ever_added(&self, "core-welcome")? { if chat::was_device_msg_ever_added(&self, "core-welcome").await? {
return Ok(()); return Ok(());
} }
// create saved-messages chat; // create saved-messages chat;
// we do this only once, if the user has deleted the chat, he can recreate it manually. // we do this only once, if the user has deleted the chat, he can recreate it manually.
if !self.sql.get_raw_config_bool(&self, "self-chat-added") { if !self.sql.get_raw_config_bool(&self, "self-chat-added").await {
self.sql self.sql
.set_raw_config_bool(&self, "self-chat-added", true)?; .set_raw_config_bool(&self, "self-chat-added", true)
chat::create_by_contact_id(&self, DC_CONTACT_ID_SELF)?; .await?;
chat::create_by_contact_id(&self, DC_CONTACT_ID_SELF).await?;
} }
// add welcome-messages. by the label, this is done only once, // add welcome-messages. by the label, this is done only once,
// if the user has deleted the message or the chat, it is not added again. // if the user has deleted the message or the chat, it is not added again.
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.text = Some(self.stock_str(DeviceMessagesHint).to_string()); msg.text = Some(self.stock_str(DeviceMessagesHint).await.to_string());
chat::add_device_msg(&self, Some("core-about-device-chat"), Some(&mut msg))?; chat::add_device_msg(&self, Some("core-about-device-chat"), Some(&mut msg)).await?;
let image = include_bytes!("../assets/welcome-image.jpg"); let image = include_bytes!("../assets/welcome-image.jpg");
let blob = BlobObject::create(&self, "welcome-image.jpg".to_string(), image)?; let blob = BlobObject::create(&self, "welcome-image.jpg".to_string(), image).await?;
let mut msg = Message::new(Viewtype::Image); let mut msg = Message::new(Viewtype::Image);
msg.param.set(Param::File, blob.as_name()); msg.param.set(Param::File, blob.as_name());
chat::add_device_msg(&self, Some("core-welcome-image"), Some(&mut msg))?; chat::add_device_msg(&self, Some("core-welcome-image"), Some(&mut msg)).await?;
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.text = Some(self.stock_str(WelcomeMessage).to_string()); msg.text = Some(self.stock_str(WelcomeMessage).await.to_string());
chat::add_device_msg(&self, Some("core-welcome"), Some(&mut msg))?; chat::add_device_msg(&self, Some("core-welcome"), Some(&mut msg)).await?;
Ok(()) Ok(())
} }
} }
@@ -388,165 +401,191 @@ mod tests {
assert_eq!(StockMessage::NoMessages.fallback(), "No messages."); assert_eq!(StockMessage::NoMessages.fallback(), "No messages.");
} }
#[test] #[async_std::test]
fn test_set_stock_translation() { async fn test_set_stock_translation() {
let t = dummy_context(); let t = dummy_context().await;
t.ctx t.ctx
.set_stock_translation(StockMessage::NoMessages, "xyz".to_string()) .set_stock_translation(StockMessage::NoMessages, "xyz".to_string())
.await
.unwrap(); .unwrap();
assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "xyz") assert_eq!(t.ctx.stock_str(StockMessage::NoMessages).await, "xyz")
} }
#[test] #[async_std::test]
fn test_set_stock_translation_wrong_replacements() { async fn test_set_stock_translation_wrong_replacements() {
let t = dummy_context(); let t = dummy_context().await;
assert!(t assert!(t
.ctx .ctx
.set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string()) .set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string())
.await
.is_err()); .is_err());
assert!(t assert!(t
.ctx .ctx
.set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string()) .set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string())
.await
.is_err()); .is_err());
} }
#[test] #[async_std::test]
fn test_stock_str() { async fn test_stock_str() {
let t = dummy_context(); let t = dummy_context().await;
assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "No messages."); assert_eq!(
t.ctx.stock_str(StockMessage::NoMessages).await,
"No messages."
);
} }
#[test] #[async_std::test]
fn test_stock_string_repl_str() { async fn test_stock_string_repl_str() {
let t = dummy_context(); let t = dummy_context().await;
// uses %1$s substitution // uses %1$s substitution
assert_eq!( assert_eq!(
t.ctx t.ctx
.stock_string_repl_str(StockMessage::MsgAddMember, "Foo"), .stock_string_repl_str(StockMessage::MsgAddMember, "Foo")
.await,
"Member Foo added." "Member Foo added."
); );
// We have no string using %1$d to test... // We have no string using %1$d to test...
} }
#[test] #[async_std::test]
fn test_stock_string_repl_int() { async fn test_stock_string_repl_int() {
let t = dummy_context(); let t = dummy_context().await;
assert_eq!( assert_eq!(
t.ctx.stock_string_repl_int(StockMessage::MsgAddMember, 42), t.ctx
.stock_string_repl_int(StockMessage::MsgAddMember, 42)
.await,
"Member 42 added." "Member 42 added."
); );
} }
#[test] #[async_std::test]
fn test_stock_string_repl_str2() { async fn test_stock_string_repl_str2() {
let t = dummy_context(); let t = dummy_context().await;
assert_eq!( assert_eq!(
t.ctx t.ctx
.stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar"), .stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar")
.await,
"Could not connect to foo: bar" "Could not connect to foo: bar"
); );
} }
#[test] #[async_std::test]
fn test_stock_system_msg_simple() { async fn test_stock_system_msg_simple() {
let t = dummy_context(); let t = dummy_context().await;
assert_eq!( assert_eq!(
t.ctx t.ctx
.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0), .stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)
.await,
"Location streaming enabled." "Location streaming enabled."
) )
} }
#[test] #[async_std::test]
fn test_stock_system_msg_add_member_by_me() { async fn test_stock_system_msg_add_member_by_me() {
let t = dummy_context(); let t = dummy_context().await;
assert_eq!( assert_eq!(
t.ctx.stock_system_msg( t.ctx
StockMessage::MsgAddMember, .stock_system_msg(
"alice@example.com", StockMessage::MsgAddMember,
"", "alice@example.com",
DC_CONTACT_ID_SELF "",
), DC_CONTACT_ID_SELF
)
.await,
"Member alice@example.com added by me." "Member alice@example.com added by me."
) )
} }
#[test] #[async_std::test]
fn test_stock_system_msg_add_member_by_me_with_displayname() { async fn test_stock_system_msg_add_member_by_me_with_displayname() {
let t = dummy_context(); let t = dummy_context().await;
Contact::create(&t.ctx, "Alice", "alice@example.com").expect("failed to create contact"); Contact::create(&t.ctx, "Alice", "alice@example.com")
.await
.expect("failed to create contact");
assert_eq!( assert_eq!(
t.ctx.stock_system_msg( t.ctx
StockMessage::MsgAddMember, .stock_system_msg(
"alice@example.com", StockMessage::MsgAddMember,
"", "alice@example.com",
DC_CONTACT_ID_SELF "",
), DC_CONTACT_ID_SELF
)
.await,
"Member Alice (alice@example.com) added by me." "Member Alice (alice@example.com) added by me."
); );
} }
#[test] #[async_std::test]
fn test_stock_system_msg_add_member_by_other_with_displayname() { async fn test_stock_system_msg_add_member_by_other_with_displayname() {
let t = dummy_context(); let t = dummy_context().await;
let contact_id = { let contact_id = {
Contact::create(&t.ctx, "Alice", "alice@example.com") Contact::create(&t.ctx, "Alice", "alice@example.com")
.await
.expect("Failed to create contact Alice"); .expect("Failed to create contact Alice");
Contact::create(&t.ctx, "Bob", "bob@example.com").expect("failed to create bob") Contact::create(&t.ctx, "Bob", "bob@example.com")
.await
.expect("failed to create bob")
}; };
assert_eq!( assert_eq!(
t.ctx.stock_system_msg( t.ctx
StockMessage::MsgAddMember, .stock_system_msg(
"alice@example.com", StockMessage::MsgAddMember,
"", "alice@example.com",
contact_id, "",
), contact_id,
)
.await,
"Member Alice (alice@example.com) added by Bob (bob@example.com)." "Member Alice (alice@example.com) added by Bob (bob@example.com)."
); );
} }
#[test] #[async_std::test]
fn test_stock_system_msg_grp_name() { async fn test_stock_system_msg_grp_name() {
let t = dummy_context(); let t = dummy_context().await;
assert_eq!( assert_eq!(
t.ctx.stock_system_msg( t.ctx
StockMessage::MsgGrpName, .stock_system_msg(
"Some chat", StockMessage::MsgGrpName,
"Other chat", "Some chat",
DC_CONTACT_ID_SELF "Other chat",
), DC_CONTACT_ID_SELF
)
.await,
"Group name changed from \"Some chat\" to \"Other chat\" by me." "Group name changed from \"Some chat\" to \"Other chat\" by me."
) )
} }
#[test] #[async_std::test]
fn test_stock_system_msg_grp_name_other() { async fn test_stock_system_msg_grp_name_other() {
let t = dummy_context(); let t = dummy_context().await;
let id = Contact::create(&t.ctx, "Alice", "alice@example.com") let id = Contact::create(&t.ctx, "Alice", "alice@example.com")
.await
.expect("failed to create contact"); .expect("failed to create contact");
assert_eq!( assert_eq!(
t.ctx t.ctx
.stock_system_msg(StockMessage::MsgGrpName, "Some chat", "Other chat", id,), .stock_system_msg(StockMessage::MsgGrpName, "Some chat", "Other chat", id)
.await,
"Group name changed from \"Some chat\" to \"Other chat\" by Alice (alice@example.com)." "Group name changed from \"Some chat\" to \"Other chat\" by Alice (alice@example.com)."
) )
} }
#[test] #[async_std::test]
fn test_update_device_chats() { async fn test_update_device_chats() {
let t = dummy_context(); let t = dummy_context().await;
t.ctx.update_device_chats().ok(); t.ctx.update_device_chats().await.ok();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2); assert_eq!(chats.len(), 2);
chats.get_chat_id(0).delete(&t.ctx).ok(); chats.get_chat_id(0).delete(&t.ctx).await.ok();
chats.get_chat_id(1).delete(&t.ctx).ok(); chats.get_chat_id(1).delete(&t.ctx).await.ok();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); assert_eq!(chats.len(), 0);
// a subsequent call to update_device_chats() must not re-add manally deleted messages or chats // a subsequent call to update_device_chats() must not re-add manally deleted messages or chats
t.ctx.update_device_chats().ok(); t.ctx.update_device_chats().await.ok();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); assert_eq!(chats.len(), 0);
} }
} }

View File

@@ -5,9 +5,8 @@
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use crate::config::Config; use crate::config::Config;
use crate::context::{Context, ContextCallback}; use crate::context::Context;
use crate::dc_tools::EmailAddress; use crate::dc_tools::EmailAddress;
use crate::events::Event;
use crate::key::{self, DcKey}; use crate::key::{self, DcKey};
/// A Context and temporary directory. /// A Context and temporary directory.
@@ -25,14 +24,10 @@ pub(crate) struct TestContext {
/// "db.sqlite" in the [TestContext.dir] directory. /// "db.sqlite" in the [TestContext.dir] directory.
/// ///
/// [Context]: crate::context::Context /// [Context]: crate::context::Context
pub(crate) fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext { pub(crate) async fn test_context() -> TestContext {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite"); let dbfile = dir.path().join("db.sqlite");
let cb: Box<ContextCallback> = match callback { let ctx = Context::new("FakeOs".into(), dbfile.into()).await.unwrap();
Some(cb) => cb,
None => Box::new(|_, _| ()),
};
let ctx = Context::new(cb, "FakeOs".into(), dbfile).unwrap();
TestContext { ctx, dir } TestContext { ctx, dir }
} }
@@ -41,17 +36,8 @@ pub(crate) fn test_context(callback: Option<Box<ContextCallback>>) -> TestContex
/// The context will be opened and use the SQLite database as /// The context will be opened and use the SQLite database as
/// specified in [test_context] but there is no callback hooked up, /// specified in [test_context] but there is no callback hooked up,
/// i.e. [Context::call_cb] will always return `0`. /// i.e. [Context::call_cb] will always return `0`.
pub(crate) fn dummy_context() -> TestContext { pub(crate) async fn dummy_context() -> TestContext {
test_context(Some(Box::new(logging_cb))) test_context().await
}
pub(crate) fn logging_cb(_ctx: &Context, evt: Event) {
match evt {
Event::Info(msg) => println!("I: {}", msg),
Event::Warning(msg) => eprintln!("=== WARNING ===\n{}\n===============", msg),
Event::Error(msg) => eprintln!("\n===================== ERROR =====================\n{}\n=================================================\n", msg),
_ => (),
}
} }
/// Load a pre-generated keypair for alice@example.com from disk. /// Load a pre-generated keypair for alice@example.com from disk.
@@ -77,11 +63,13 @@ pub(crate) fn alice_keypair() -> key::KeyPair {
/// Creates Alice with a pre-generated keypair. /// Creates Alice with a pre-generated keypair.
/// ///
/// Returns the address of the keypair created (alice@example.com). /// Returns the address of the keypair created (alice@example.com).
pub(crate) fn configure_alice_keypair(ctx: &Context) -> String { pub(crate) async fn configure_alice_keypair(ctx: &Context) -> String {
let keypair = alice_keypair(); let keypair = alice_keypair();
ctx.set_config(Config::ConfiguredAddr, Some(&keypair.addr.to_string())) ctx.set_config(Config::ConfiguredAddr, Some(&keypair.addr.to_string()))
.await
.unwrap(); .unwrap();
key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default) key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default)
.await
.expect("Failed to save Alice's key"); .expect("Failed to save Alice's key");
keypair.addr.to_string() keypair.addr.to_string()
} }

View File

@@ -9,7 +9,6 @@ use deltachat_derive::*;
use crate::chat::ChatId; use crate::chat::ChatId;
use crate::context::Context; use crate::context::Context;
use crate::dc_tools::*; use crate::dc_tools::*;
use crate::sql;
/// Token namespace /// Token namespace
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)] #[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
@@ -28,37 +27,46 @@ impl Default for Namespace {
/// Creates a new token and saves it into the database. /// Creates a new token and saves it into the database.
/// Returns created token. /// Returns created token.
pub fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String { pub async fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String {
// foreign_id may be 0 // foreign_id may be 0
let token = dc_create_id(); let token = dc_create_id();
sql::execute( context
context, .sql
&context.sql, .execute(
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);", "INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
params![namespace, foreign_id, &token, time()], paramsv![namespace, foreign_id, token, time()],
) )
.ok(); .await
.ok();
token token
} }
pub fn lookup(context: &Context, namespace: Namespace, foreign_id: ChatId) -> Option<String> { pub async fn lookup(context: &Context, namespace: Namespace, foreign_id: ChatId) -> Option<String> {
context.sql.query_get_value::<_, String>( context
context, .sql
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;", .query_get_value::<String>(
params![namespace, foreign_id], context,
) "SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;",
paramsv![namespace, foreign_id],
)
.await
} }
pub fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String { pub async fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String {
lookup(context, namespace, foreign_id).unwrap_or_else(|| save(context, namespace, foreign_id)) if let Some(token) = lookup(context, namespace, foreign_id).await {
return token;
}
save(context, namespace, foreign_id).await
} }
pub fn exists(context: &Context, namespace: Namespace, token: &str) -> bool { pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> bool {
context context
.sql .sql
.exists( .exists(
"SELECT id FROM tokens WHERE namespc=? AND token=?;", "SELECT id FROM tokens WHERE namespc=? AND token=?;",
params![namespace, token], paramsv![namespace, token],
) )
.await
.unwrap_or_default() .unwrap_or_default()
} }

View File

@@ -2,14 +2,16 @@
use deltachat::config; use deltachat::config;
use deltachat::context::*; use deltachat::context::*;
use deltachat::Event;
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
/* some data used for testing /* some data used for testing
******************************************************************************/ ******************************************************************************/
fn stress_functions(context: &Context) { async fn stress_functions(context: &Context) {
let res = context.get_config(config::Config::SysConfigKeys).unwrap(); let res = context
.get_config(config::Config::SysConfigKeys)
.await
.unwrap();
assert!(!res.contains(" probably_never_a_key ")); assert!(!res.contains(" probably_never_a_key "));
assert!(res.contains(" addr ")); assert!(res.contains(" addr "));
@@ -90,23 +92,21 @@ fn stress_functions(context: &Context) {
// free(qr.cast()); // free(qr.cast());
} }
fn cb(_context: &Context, _event: Event) {}
#[allow(dead_code)] #[allow(dead_code)]
struct TestContext { struct TestContext {
ctx: Context, ctx: Context,
dir: TempDir, dir: TempDir,
} }
fn create_test_context() -> TestContext { async fn create_test_context() -> TestContext {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite"); let dbfile = dir.path().join("db.sqlite");
let ctx = Context::new(Box::new(cb), "FakeOs".into(), dbfile).unwrap(); let ctx = Context::new("FakeOs".into(), dbfile.into()).await.unwrap();
TestContext { ctx, dir } TestContext { ctx, dir }
} }
#[test] #[async_std::test]
fn test_stress_tests() { async fn test_stress_tests() {
let context = create_test_context(); let context = create_test_context().await;
stress_functions(&context.ctx); stress_functions(&context.ctx).await;
} }